diff options
Diffstat (limited to 'browser/components/places/tests')
159 files changed, 22412 insertions, 0 deletions
diff --git a/browser/components/places/tests/browser/bookmark_dummy_1.html b/browser/components/places/tests/browser/bookmark_dummy_1.html new file mode 100644 index 0000000000..c03e0c18c0 --- /dev/null +++ b/browser/components/places/tests/browser/bookmark_dummy_1.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Bookmark Dummy 1</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Bookmark Dummy 1</p> +</body> +</html> diff --git a/browser/components/places/tests/browser/bookmark_dummy_2.html b/browser/components/places/tests/browser/bookmark_dummy_2.html new file mode 100644 index 0000000000..229a730b32 --- /dev/null +++ b/browser/components/places/tests/browser/bookmark_dummy_2.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Bookmark Dummy 2</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Bookmark Dummy 2</p> +</body> +</html> diff --git a/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html b/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html new file mode 100644 index 0000000000..54a87d3247 --- /dev/null +++ b/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Bookmarklet windowOpen Dummy</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Bookmarklet windowOpen Dummy</p> +</body> +</html> diff --git a/browser/components/places/tests/browser/browser.toml b/browser/components/places/tests/browser/browser.toml new file mode 100644 index 0000000000..1b0e2571d4 --- /dev/null +++ b/browser/components/places/tests/browser/browser.toml @@ -0,0 +1,236 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[DEFAULT] +support-files = [ + "head.js", + "framedPage.html", + "frameLeft.html", + "frameRight.html", + "sidebarpanels_click_test_page.html", + "keyword_form.html", +] + +["browser_addBookmarkForFrame.js"] + +["browser_autoshow_bookmarks_toolbar.js"] + +["browser_bookmarkMenu_hiddenWindow.js"] +skip-if = ["os != 'mac'"] # Mac-only functionality + +["browser_bookmarkProperties_addFolderDefaultButton.js"] + +["browser_bookmarkProperties_addKeywordForThisSearch.js"] + +["browser_bookmarkProperties_bookmarkAllTabs.js"] + +["browser_bookmarkProperties_cancel.js"] + +["browser_bookmarkProperties_editFolder.js"] + +["browser_bookmarkProperties_editTagContainer.js"] + +["browser_bookmarkProperties_folderSelection.js"] + +["browser_bookmarkProperties_newFolder.js"] + +["browser_bookmarkProperties_no_user_actions.js"] + +["browser_bookmarkProperties_readOnlyRoot.js"] + +["browser_bookmarkProperties_remember_folders.js"] + +["browser_bookmarkProperties_speculativeConnection.js"] + +["browser_bookmarkProperties_xulStore.js"] + +["browser_bookmark_add_tags.js"] +https_first_disabled = true + +["browser_bookmark_all_tabs.js"] +https_first_disabled = true +support-files = [ + "bookmark_dummy_1.html", + "bookmark_dummy_2.html", +] + +["browser_bookmark_backup_export_import.js"] + +["browser_bookmark_change_location.js"] + +["browser_bookmark_context_menu_contents.js"] + +["browser_bookmark_copy_folder_tree.js"] + +["browser_bookmark_folder_moveability.js"] + +["browser_bookmark_menu_ctrl_click.js"] + +["browser_bookmark_popup.js"] +skip-if = ["verify && os == 'win'"] + +["browser_bookmark_private_window.js"] + +["browser_bookmark_remove_tags.js"] + +["browser_bookmark_titles.js"] +https_first_disabled = true +support-files = ["../../../../base/content/test/general/dummy_page.html"] + +["browser_bookmarklet_windowOpen.js"] +support-files = ["bookmarklet_windowOpen_dummy.html"] + +["browser_bookmarksProperties.js"] + +["browser_bookmarks_change_title.js"] + +["browser_bookmarks_change_url.js"] + +["browser_bookmarks_sidebar_search.js"] +support-files = ["pageopeningwindow.html"] + +["browser_bookmarks_toolbar_context_menu_view_options.js"] + +["browser_bookmarks_toolbar_telemetry.js"] + +["browser_bug427633_no_newfolder_if_noip.js"] + +["browser_bug485100-change-case-loses-tag.js"] + +["browser_bug631374_tags_selector_scroll.js"] +support-files = ["favicon-normal16.png"] + +["browser_check_correct_controllers.js"] + +["browser_click_bookmarks_on_toolbar.js"] +https_first_disabled = true + +["browser_controller_onDrop.js"] + +["browser_controller_onDrop_query.js"] + +["browser_controller_onDrop_sidebar.js"] + +["browser_controller_onDrop_tagFolder.js"] + +["browser_copy_query_without_tree.js"] + +["browser_cutting_bookmarks.js"] + +["browser_default_bookmark_location.js"] + +["browser_drag_bookmarks_on_toolbar.js"] + +["browser_drag_folder_on_newTab.js"] +https_first_disabled = true + +["browser_editBookmark_keywords.js"] + +["browser_enable_toolbar_sidebar.js"] +skip-if = ["verify && debug && os == 'win'"] + +["browser_forgetthissite.js"] + +["browser_history_sidebar_search.js"] + +["browser_import_button.js"] + +["browser_library_bookmark_clear_visits.js"] + +["browser_library_bookmark_pages.js"] + +["browser_library_bulk_tag_bookmarks.js"] + +["browser_library_commands.js"] + +["browser_library_delete.js"] + +["browser_library_delete_bookmarks_in_tags.js"] + +["browser_library_delete_tags.js"] + +["browser_library_downloads.js"] + +["browser_library_left_pane_middleclick.js"] + +["browser_library_left_pane_select_hierarchy.js"] + +["browser_library_middleclick.js"] + +["browser_library_new_bookmark.js"] + +["browser_library_openFlatContainer.js"] + +["browser_library_open_all.js"] + +["browser_library_open_all_with_separator.js"] + +["browser_library_open_bookmark.js"] + +["browser_library_open_leak.js"] + +["browser_library_panel_leak.js"] + +["browser_library_sameNodeDetailsPaneOptimization.js"] + +["browser_library_search.js"] + +["browser_library_tags_visibility.js"] + +["browser_library_telemetry.js"] + +["browser_library_tree_leak.js"] + +["browser_library_views_liveupdate.js"] + +["browser_library_warnOnOpen.js"] + +["browser_markPageAsFollowedLink.js"] + +["browser_panelview_bookmarks_delete.js"] + +["browser_paste_bookmarks.js"] + +["browser_paste_into_tags.js"] + +["browser_paste_resets_cut_highlights.js"] + +["browser_remove_bookmarks.js"] + +["browser_sidebar_bookmarks_telemetry.js"] + +["browser_sidebar_history_telemetry.js"] + +["browser_sidebar_on_customization.js"] + +["browser_sidebar_open_bookmarks.js"] + +["browser_sidebarpanels_click.js"] + +["browser_sort_in_library.js"] + +["browser_stayopenmenu.js"] + +["browser_toolbar_drop_bookmarklet.js"] + +["browser_toolbar_drop_multiple_flavors.js"] + +["browser_toolbar_drop_multiple_with_bookmarklet.js"] + +["browser_toolbar_drop_text.js"] + +["browser_toolbar_library_open_recent.js"] +https_first_disabled = true + +["browser_toolbar_other_bookmarks.js"] + +["browser_toolbar_overflow.js"] + +["browser_toolbarbutton_menu_context.js"] + +["browser_toolbarbutton_menu_show_in_folder.js"] + +["browser_views_iconsupdate.js"] + +["browser_views_liveupdate.js"] diff --git a/browser/components/places/tests/browser/browser_addBookmarkForFrame.js b/browser/components/places/tests/browser/browser_addBookmarkForFrame.js new file mode 100644 index 0000000000..44a7f5e17a --- /dev/null +++ b/browser/components/places/tests/browser/browser_addBookmarkForFrame.js @@ -0,0 +1,150 @@ +/** + * Tests that the add bookmark for frame dialog functions correctly. + */ + +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/places/tests/browser"; +const PAGE_URL = BASE_URL + "/framedPage.html"; +const LEFT_URL = BASE_URL + "/frameLeft.html"; +const RIGHT_URL = BASE_URL + "/frameRight.html"; + +function activateBookmarkFrame(contentAreaContextMenu) { + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contentAreaContextMenu, + "popuphidden" + ); + return async function () { + let frameMenuItem = document.getElementById("frame"); + let frameMenu = frameMenuItem.querySelector(":scope > menupopup"); + let frameMenuShown = BrowserTestUtils.waitForEvent(frameMenu, "popupshown"); + frameMenuItem.openMenu(true); + await frameMenuShown; + let bookmarkFrame = document.getElementById("context-bookmarkframe"); + frameMenu.activateItem(bookmarkFrame); + await popupHiddenPromise; + }; +} + +async function withAddBookmarkForFrame(taskFn) { + // Open a tab and wait for all the subframes to load. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + + let contentAreaContextMenu = document.getElementById( + "contentAreaContextMenu" + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + contentAreaContextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#left", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupShownPromise; + + await withBookmarksDialog( + true, + activateBookmarkFrame(contentAreaContextMenu), + taskFn + ); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function test_open_add_bookmark_for_frame() { + info("Test basic opening of the add bookmark for frame dialog."); + await withAddBookmarkForFrame(async dialogWin => { + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.ok(!namepicker.readOnly, "Name field is writable"); + Assert.equal(namepicker.value, "Left frame", "Name field is correct."); + + let expectedFolder = "BookmarksToolbarFolderTitle"; + let expectedFolderName = PlacesUtils.getString(expectedFolder); + + let folderPicker = dialogWin.document.getElementById( + "editBMPanel_folderMenuList" + ); + + await TestUtils.waitForCondition( + () => folderPicker.selectedItem.label == expectedFolderName, + "Dialog: The folder is the expected one." + ); + + let tagsField = dialogWin.document.getElementById("editBMPanel_tagsField"); + Assert.equal(tagsField.value, "", "Dialog: The tags field should be empty"); + }); +}); + +add_task(async function test_move_bookmark_whilst_add_bookmark_open() { + info( + "EditBookmark: Test moving a bookmark whilst the add bookmark for frame dialog is open." + ); + await PlacesUtils.bookmarks.eraseEverything(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + + let contentAreaContextMenu = document.getElementById( + "contentAreaContextMenu" + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + contentAreaContextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#left", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupShownPromise; + + await withBookmarksDialog( + false, + activateBookmarkFrame(contentAreaContextMenu), + async function (dialogWin) { + let expectedGuid = await PlacesUIUtils.defaultParentGuid; + let expectedFolder = "BookmarksToolbarFolderTitle"; + let expectedFolderName = PlacesUtils.getString(expectedFolder); + + let folderPicker = dialogWin.document.getElementById( + "editBMPanel_folderMenuList" + ); + + Assert.equal( + folderPicker.selectedItem.label, + expectedFolderName, + "EditBookmark: The folder is the expected one." + ); + + Assert.equal( + folderPicker.getAttribute("selectedGuid"), + expectedGuid, + "EditBookmark: Should have the correct default guid selected" + ); + + dialogWin.document.getElementById("editBMPanel_foldersExpander").click(); + let folderTree = dialogWin.document.getElementById( + "editBMPanel_folderTree" + ); + folderTree.selectItems([PlacesUtils.bookmarks.menuGuid]); + folderTree.blur(); + + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + } + ); + let url = makeURI(LEFT_URL); + // Check the bookmark has been moved as expected. + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + + Assert.equal( + bookmark.parentGuid, + PlacesUtils.bookmarks.menuGuid, + "EditBookmark: The bookmark should be moved to the expected folder." + ); + + BrowserTestUtils.removeTab(tab); + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js b/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js new file mode 100644 index 0000000000..c841eb276b --- /dev/null +++ b/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LOCATION_PREF = "browser.bookmarks.defaultLocation"; +const TOOLBAR_VISIBILITY_PREF = "browser.toolbars.bookmarks.visibility"; +let bookmarkPanel; +let win; + +add_setup(async function () { + Services.prefs.clearUserPref(LOCATION_PREF); + await PlacesUtils.bookmarks.eraseEverything(); + + win = await BrowserTestUtils.openNewBrowserWindow(); + + let oldTimeout = win.StarUI._autoCloseTimeout; + // Make the timeout something big, so it doesn't interact badly with tests. + win.StarUI._autoCloseTimeout = 6000000; + + win.StarUI._createPanelIfNeeded(); + bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + + registerCleanupFunction(async () => { + bookmarkPanel = null; + win.StarUI._autoCloseTimeout = oldTimeout; + await BrowserTestUtils.closeWindow(win); + win = null; + await PlacesUtils.bookmarks.eraseEverything(); + Services.prefs.clearUserPref(LOCATION_PREF); + }); +}); + +/** + * Helper to check we've shown the toolbar + * + * @param {object} options + * Options for the test + * @param {boolean} options.showToolbar + * If the toolbar should be shown or not + * @param {string} options.expectedFolder + * The expected folder to be shown + * @param {string} options.reason + * The reason the toolbar should be shown + */ +async function checkResponse({ showToolbar, expectedFolder, reason }) { + // Check folder. + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + `Should have ${expectedFolder} selected ${reason}.` + ); + + // Check toolbar: + let toolbar = win.document.getElementById("PersonalToolbar"); + Assert.equal( + !toolbar.collapsed, + showToolbar, + `Toolbar should be ${showToolbar ? "visible" : "hidden"} ${reason}.` + ); + + // Confirm and close the dialog. + let hiddenPromise = promisePopupHidden( + win.document.getElementById("editBookmarkPanel") + ); + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + await hiddenPromise; +} + +/** + * Test that if we create a bookmark on the toolbar, we show the + * toolbar: + */ +add_task(async function test_new_on_toolbar() { + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "https://example.com/1" }, + async browser => { + let toolbar = win.document.getElementById("PersonalToolbar"); + Assert.equal( + toolbar.collapsed, + true, + "Bookmarks toolbar should start out collapsed." + ); + let shownPromise = promisePopupShown( + win.document.getElementById("editBookmarkPanel") + ); + win.document.getElementById("Browser:AddBookmarkAs").doCommand(); + await shownPromise; + await TestUtils.waitForCondition( + () => !toolbar.collapsed, + "Toolbar should be shown." + ); + let expectedFolder = "BookmarksToolbarFolderTitle"; + let reason = "when creating a bookmark there"; + await checkResponse({ showToolbar: true, expectedFolder, reason }); + } + ); +}); + +/** + * Test that if we create a bookmark on the toolbar, we do not + * show the toolbar if toolbar should never be shown: + */ +add_task(async function test_new_on_toolbar_never_show_toolbar() { + await SpecialPowers.pushPrefEnv({ + set: [[TOOLBAR_VISIBILITY_PREF, "never"]], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "https://example.com/1" }, + async browser => { + let toolbar = win.document.getElementById("PersonalToolbar"); + Assert.equal( + toolbar.collapsed, + true, + "Bookmarks toolbar should start out collapsed." + ); + let shownPromise = promisePopupShown( + win.document.getElementById("editBookmarkPanel") + ); + win.document.getElementById("Browser:AddBookmarkAs").doCommand(); + await shownPromise; + let expectedFolder = "BookmarksToolbarFolderTitle"; + let reason = "when the visibility pref is 'never'"; + await checkResponse({ showToolbar: false, expectedFolder, reason }); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Test that if we edit an existing bookmark, we don't show the toolbar. + */ +add_task(async function test_existing_on_toolbar() { + // Create the bookmark first: + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Test for editing", + url: "https://example.com/editing-test", + }); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "https://example.com/editing-test" }, + async browser => { + await TestUtils.waitForCondition( + () => win.BookmarkingUI.status == BookmarkingUI.STATUS_STARRED, + "Page should be starred." + ); + + let toolbar = win.document.getElementById("PersonalToolbar"); + Assert.equal( + toolbar.collapsed, + true, + "Bookmarks toolbar should start out collapsed." + ); + await clickBookmarkStar(win); + let expectedFolder = "BookmarksToolbarFolderTitle"; + let reason = "when editing a bookmark there"; + await checkResponse({ showToolbar: false, expectedFolder, reason }); + } + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js b/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js new file mode 100644 index 0000000000..4502b65e69 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_setup(async function () { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://example.com/", + title: "Test1", + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_menu_in_hidden_window() { + let hwDoc = Services.appShell.hiddenDOMWindow.document; + let bmPopup = hwDoc.getElementById("bookmarksMenuPopup"); + var popupEvent = hwDoc.createEvent("MouseEvent"); + popupEvent.initMouseEvent( + "popupshowing", + true, + true, + Services.appShell.hiddenDOMWindow, + 0, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + bmPopup.dispatchEvent(popupEvent); + + let testMenuitem = [...bmPopup.children].find( + node => node.getAttribute("label") == "Test1" + ); + Assert.ok( + testMenuitem, + "Should have found the test bookmark in the hidden window bookmark menu" + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js new file mode 100644 index 0000000000..0c04dbd243 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js @@ -0,0 +1,68 @@ +"use strict"; + +add_task(async function add_folder_default_button() { + info( + "Bug 475529 - Add is the default button for the new folder dialog + " + + "Bug 1206376 - Changing properties of a new bookmark while adding it " + + "acts on the last bookmark in the current container" + ); + + // Add a new bookmark at index 0 in the unfiled folder. + let insertionIndex = 0; + let newBookmark = await PlacesUtils.bookmarks.insert({ + index: insertionIndex, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + }); + + await withSidebarTree("bookmarks", async function (tree) { + // Select the new bookmark in the sidebar. + tree.selectItems([newBookmark.guid]); + Assert.ok( + tree.controller.isCommandEnabled("placesCmd_new:folder"), + "'placesCmd_new:folder' on current selected node is enabled" + ); + + // Create a new folder. Since the new bookmark is selected, and new items + // are inserted at the index of the currently selected item, the new folder + // will be inserted at index 0. + await withBookmarksDialog( + false, + function openDialog() { + tree.controller.doCommand("placesCmd_new:folder"); + }, + async function test(dialogWin) { + const notifications = [ + PlacesTestUtils.waitForNotification("bookmark-added", events => + events.some(e => e.title === "n") + ), + PlacesTestUtils.waitForNotification("bookmark-moved", null), + ]; + + fillBookmarkTextField("editBMPanel_namePicker", "n", dialogWin, false); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + await Promise.all(notifications); + + let newFolder = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: insertionIndex, + }); + + is(newFolder.title, "n", "folder name has been edited"); + + let bm = await PlacesUtils.bookmarks.fetch(newBookmark.guid); + Assert.equal( + bm.index, + insertionIndex + 1, + "Bookmark should have been shifted to the next index" + ); + + await PlacesUtils.bookmarks.remove(newFolder); + await PlacesUtils.bookmarks.remove(newBookmark); + } + ); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js new file mode 100644 index 0000000000..514519810a --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js @@ -0,0 +1,188 @@ +"use strict"; + +const TEST_URL = + "http://mochi.test:8888/browser/browser/components/places/tests/browser/keyword_form.html"; + +let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + +add_task(async function add_keyword() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function (browser) { + // We must wait for the context menu code to build metadata. + await openContextMenuForContentSelector( + browser, + '#form1 > input[name="search"]' + ); + + await withBookmarksDialog( + false, + function () { + AddKeywordForSearchField(); + contentAreaContextMenu.hidePopup(); + }, + async function (dialogWin) { + let acceptBtn = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + Assert.ok(acceptBtn.disabled, "Accept button is disabled"); + + let promiseKeywordNotification = PlacesTestUtils.waitForNotification( + "bookmark-keyword-changed", + events => events.some(event => event.keyword === "kw") + ); + + fillBookmarkTextField("editBMPanel_keywordField", "kw", dialogWin); + + Assert.ok(!acceptBtn.disabled, "Accept button is enabled"); + + acceptBtn.click(); + await promiseKeywordNotification; + + // After the notification, the keywords cache will update asynchronously. + info("Check the keyword entry has been created"); + let entry; + await TestUtils.waitForCondition(async function () { + entry = await PlacesUtils.keywords.fetch("kw"); + return !!entry; + }, "Unable to find the expected keyword"); + Assert.equal(entry.keyword, "kw", "keyword is correct"); + Assert.equal(entry.url.href, TEST_URL, "URL is correct"); + Assert.equal( + entry.postData, + "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s", + "POST data is correct" + ); + let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL }); + Assert.equal( + bm.parentGuid, + await PlacesUIUtils.defaultParentGuid, + "Should have created the keyword in the right folder." + ); + + info("Check the charset has been saved"); + let pageInfo = await PlacesUtils.history.fetch(TEST_URL, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + "windows-1252", + "charset is correct" + ); + + // Now check getShortcutOrURI. + let data = await UrlbarUtils.getShortcutOrURIAndPostData("kw test"); + Assert.equal( + getPostDataString(data.postData), + "accenti=\u00E0\u00E8\u00EC\u00F2\u00F9&search=test", + "getShortcutOrURI POST data is correct" + ); + Assert.equal(data.url, TEST_URL, "getShortcutOrURI URL is correct"); + }, + () => PlacesUtils.bookmarks.eraseEverything() + ); + } + ); +}); + +add_task(async function reopen_same_field() { + await PlacesUtils.keywords.insert({ + url: TEST_URL, + keyword: "kw", + postData: "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s", + }); + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("kw"); + }); + // Reopening on the same input field should show the existing keyword. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function (browser) { + // We must wait for the context menu code to build metadata. + await openContextMenuForContentSelector( + browser, + '#form1 > input[name="search"]' + ); + + await withBookmarksDialog( + true, + function () { + AddKeywordForSearchField(); + contentAreaContextMenu.hidePopup(); + }, + async function (dialogWin) { + let elt = dialogWin.document.getElementById( + "editBMPanel_keywordField" + ); + Assert.equal(elt.value, "kw", "Keyword should be the previous value"); + + let acceptBtn = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + ok(!acceptBtn.disabled, "Accept button is enabled"); + }, + () => PlacesUtils.bookmarks.eraseEverything() + ); + } + ); +}); + +add_task(async function open_other_field() { + await PlacesUtils.keywords.insert({ + url: TEST_URL, + keyword: "kw2", + postData: "search%3D%25s", + }); + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("kw2"); + }); + // Reopening on another field of the same page that has different postData + // should not show the existing keyword. + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function (browser) { + // We must wait for the context menu code to build metadata. + await openContextMenuForContentSelector( + browser, + '#form2 > input[name="search"]' + ); + + await withBookmarksDialog( + true, + function () { + AddKeywordForSearchField(); + contentAreaContextMenu.hidePopup(); + }, + function (dialogWin) { + let acceptBtn = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + ok(acceptBtn.disabled, "Accept button is disabled"); + + let elt = dialogWin.document.getElementById( + "editBMPanel_keywordField" + ); + is(elt.value, ""); + }, + () => PlacesUtils.bookmarks.eraseEverything() + ); + } + ); +}); + +function getPostDataString(stream) { + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(stream); + return sis.read(stream.available()).split("\n").pop(); +} diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js b/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js new file mode 100644 index 0000000000..805f9464e3 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js @@ -0,0 +1,66 @@ +"use strict"; + +const TEST_URLS = ["about:robots", "about:mozilla"]; + +add_task(async function bookmark_all_tabs() { + let tabs = []; + for (let url of TEST_URLS) { + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url)); + } + registerCleanupFunction(async function () { + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await withBookmarksDialog( + false, + function open() { + document.getElementById("Browser:BookmarkAllTabs").doCommand(); + }, + async dialog => { + let acceptBtn = dialog.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + Assert.ok(!acceptBtn.disabled, "Accept button is enabled"); + + let namepicker = dialog.document.getElementById("editBMPanel_namePicker"); + Assert.ok(!namepicker.readOnly, "Name field is writable"); + let folderName = dialog.document + .getElementById("stringBundle") + .getString("bookmarkAllTabsDefault"); + Assert.equal(namepicker.value, folderName, "Name field is correct."); + + let promiseBookmarkAdded = + PlacesTestUtils.waitForNotification("bookmark-added"); + + fillBookmarkTextField("editBMPanel_namePicker", "folder", dialog); + + let folderPicker = dialog.document.getElementById( + "editBMPanel_folderMenuList" + ); + + let defaultParentGuid = await PlacesUIUtils.defaultParentGuid; + // Check the initial state of the folder picker. + await TestUtils.waitForCondition( + () => folderPicker.getAttribute("selectedGuid") == defaultParentGuid, + "The folder is the expected one." + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, dialog); + await promiseBookmarkAdded; + for (const url of TEST_URLS) { + const { parentGuid } = await PlacesUtils.bookmarks.fetch({ url }); + const folder = await PlacesUtils.bookmarks.fetch({ + guid: parentGuid, + }); + is( + folder.title, + "folder", + "Should have created the bookmark in the right folder." + ); + } + } + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js b/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js new file mode 100644 index 0000000000..5652358acb --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js @@ -0,0 +1,126 @@ +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const sandbox = sinon.createSandbox(); + +registerCleanupFunction(async function () { + sandbox.restore(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +let bookmarks; // Bookmarks added via insertTree. + +add_setup(async function () { + bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "http://example.com", + }, + { + title: "bm2", + url: "http://example.com/2", + }, + ], + }); + + // Undo is called asynchronously - and not waited for. Since we're not + // expecting undo to be called, we can only tell this by stubbing it. + sandbox.stub(PlacesTransactions, "undo").returns(Promise.resolve()); +}); + +// Tests for bug 1391393 - Ensures that if the user cancels the bookmark properties +// dialog without having done any changes, then no undo is called. +add_task(async function test_cancel_with_no_changes() { + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([bookmarks[0].guid]); + + // Delete the bookmark to put something in the undo history. + // Rather than calling cmd_delete, we call the remove directly, so that we + // can await on it finishing, and be guaranteed that there's something + // in the history. + await tree.controller.remove("Remove Selection"); + + tree.selectItems([bookmarks[1].guid]); + + // Now open the bookmarks dialog and cancel it. + await withBookmarksDialog( + true, + function openDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + let acceptButton = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + await TestUtils.waitForCondition( + () => !acceptButton.disabled, + "The accept button should be enabled" + ); + } + ); + + // Check the bookmark is still removed. + Assert.ok( + !(await PlacesUtils.bookmarks.fetch(bookmarks[0].guid)), + "The originally removed bookmark should not exist." + ); + + Assert.ok( + await PlacesUtils.bookmarks.fetch(bookmarks[1].guid), + "The second bookmark should still exist" + ); + + Assert.ok( + PlacesTransactions.undo.notCalled, + "undo should not have been called" + ); + }); +}); + +add_task(async function test_cancel_with_changes() { + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([bookmarks[1].guid]); + + // Now open the bookmarks dialog and cancel it. + await withBookmarksDialog( + true, + function openDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + let acceptButton = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + await TestUtils.waitForCondition( + () => !acceptButton.disabled, + "EditBookmark: The accept button should be enabled" + ); + + let namePicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + fillBookmarkTextField("editBMPanel_namePicker", "new_n", dialogWin); + + // Ensure that value in field has changed + Assert.equal( + namePicker.value, + "new_n", + "EditBookmark: The title is the expected one." + ); + } + ); + + let oldBookmark = await PlacesUtils.bookmarks.fetch(bookmarks[1].guid); + Assert.equal( + oldBookmark.title, + "bm2", + "EditBookmark: The title hasn't been changed" + ); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js new file mode 100644 index 0000000000..fae9d01bec --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the Properties dialog on a folder. + +add_task(async function test_bookmark_properties_dialog_on_folder() { + info("Bug 479348 - Properties on a root should be read-only."); + + let bm = await PlacesUtils.bookmarks.insert({ + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + await withSidebarTree("bookmarks", async function (tree) { + // Select the new bookmark in the sidebar. + tree.selectItems([bm.guid]); + let folder = tree.selectedNode; + Assert.equal(folder.title, "folder", "Folder title is correct"); + Assert.ok( + tree.controller.isCommandEnabled("placesCmd_show:info"), + "'placesCmd_show:info' on folder is enabled" + ); + + await withBookmarksDialog( + false, + function openDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + // Check that the dialog is not read-only. + Assert.ok( + !dialogWin.gEditItemOverlay.readOnly, + "EditBookmark: Dialog should not be read-only" + ); + + // Check that name picker is not read only + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.ok( + !namepicker.readOnly, + "EditBookmark: Name field should not be read-only" + ); + Assert.equal( + namepicker.value, + "folder", + "EditBookmark:Node title is correct" + ); + + fillBookmarkTextField("editBMPanel_namePicker", "newname", dialogWin); + namepicker.blur(); + + Assert.equal( + namepicker.value, + "newname", + "EditBookmark: The title field has been changed" + ); + + // Confirm and close the dialog. + namepicker.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + // Ensure that the edit is finished before we hit cancel. + } + ); + + Assert.equal( + tree.selectedNode.title, + "newname", + "EditBookmark: The node has the correct title" + ); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js new file mode 100644 index 0000000000..f4e3b3e3fe --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js @@ -0,0 +1,140 @@ +"use strict"; + +add_task(async function editTagContainer() { + info("Bug 479348 - Properties on a root should be read-only."); + let uri = Services.io.newURI("http://example.com/"); + let bm = await PlacesUtils.bookmarks.insert({ + url: uri.spec, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + PlacesUtils.tagging.tagURI(uri, ["tag1"]); + + let library = await promiseLibrary(); + let PlacesOrganizer = library.PlacesOrganizer; + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + + PlacesOrganizer.selectLeftPaneBuiltIn("Tags"); + let tree = PlacesOrganizer._places; + let tagsContainer = tree.selectedNode; + tagsContainer.containerOpen = true; + let fooTag = tagsContainer.getChild(0); + let tagNode = fooTag; + tree.selectNode(fooTag); + Assert.equal(tagNode.title, "tag1", "EditBookmark: tagNode title is correct"); + + Assert.ok( + tree.controller.isCommandEnabled("placesCmd_show:info"), + "EditBookmark: 'placesCmd_show:info' on current selected node is enabled" + ); + + await withBookmarksDialog( + true, + function openDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + // Check that the dialog is not read-only. + Assert.ok( + !dialogWin.gEditItemOverlay.readOnly, + "EditBookmark: Dialog should not be read-only" + ); + + // Check that name picker is not read only + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.ok( + !namepicker.readOnly, + "EditBookmark: Name field should not be read-only" + ); + Assert.equal( + namepicker.value, + "tag1", + "EditBookmark: Node title is correct" + ); + + fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin); + + // Although we have received the expected notifications, we need + // to let everything resolve to ensure the UI is updated. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + Assert.equal( + namepicker.value, + "tag2", + "EditBookmark: The title field has been changed" + ); + + // Try to set an empty title, it should restore the previous one. + fillBookmarkTextField("editBMPanel_namePicker", "", dialogWin); + + Assert.equal( + namepicker.value, + "tag1", + "EditBookmark: The title field has been changed" + ); + } + ); + + // Check the tag change hasn't changed + let tags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(tags.length, 1, "EditBookmark: Found the right number of tags"); + Assert.deepEqual( + tags, + ["tag1"], + "EditBookmark: Found the expected unchanged tag" + ); + + await withBookmarksDialog( + false, + function openDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + // Check that the dialog is not read-only. + Assert.ok( + !dialogWin.gEditItemOverlay.readOnly, + "Dialog should not be read-only" + ); + + // Check that name picker is not read only + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.ok( + !namepicker.readOnly, + "EditBookmark: Name field should not be read-only" + ); + Assert.equal( + namepicker.value, + "tag1", + "EditBookmark: Node title is correct" + ); + + fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin); + + Assert.equal( + namepicker.value, + "tag2", + "EditBookmark: The title field has been changed" + ); + namepicker.blur(); + + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + } + ); + + tags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(tags.length, 1, "EditBookmark: Found the right number of tags"); + Assert.deepEqual( + tags, + ["tag2"], + "EditBookmark: Found the expected Y changed tag" + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js b/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js new file mode 100644 index 0000000000..39f3dd8822 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_URL = "about:robots"; +let bookmarkPanel; +let folders; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + Services.prefs.clearUserPref("browser.bookmarks.defaultLocation"); + + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + let oldTimeout = StarUI._autoCloseTimeout; + // Make the timeout something big, so it doesn't iteract badly with tests. + StarUI._autoCloseTimeout = 6000000; + + StarUI._createPanelIfNeeded(); + bookmarkPanel = document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + + registerCleanupFunction(async () => { + bookmarkPanel = null; + StarUI._autoCloseTimeout = oldTimeout; + BrowserTestUtils.removeTab(tab); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_selectChoose() { + await clickBookmarkStar(); + + // Open folder selector. + let menuList = document.getElementById("editBMPanel_folderMenuList"); + let folderTreeRow = document.getElementById("editBMPanel_folderTreeRow"); + + let expectedFolder = "BookmarksToolbarFolderTitle"; + let expectedGuid = PlacesUtils.bookmarks.toolbarGuid; + + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + "Should have the expected bookmarks folder selected by default" + ); + Assert.equal( + menuList.getAttribute("selectedGuid"), + expectedGuid, + "Should have the correct default guid selected" + ); + Assert.equal( + folderTreeRow.hidden, + true, + "Should have the folder tree hidden" + ); + + let promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}); + await promisePopup; + + // Click the choose item. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_chooseFolderMenuItem"), + {} + ); + + await TestUtils.waitForCondition( + () => !folderTreeRow.hidden, + "Should show the folder tree" + ); + let folderTree = document.getElementById("editBMPanel_folderTree"); + Assert.ok(folderTree.view, "The view should have been connected"); + + Assert.equal( + menuList.getAttribute("selectedGuid"), + expectedGuid, + "Should still have the correct selected guid" + ); + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + "Should have kept the same menu label" + ); + + let input = folderTree.shadowRoot.querySelector("input"); + + let newFolderButton = document.getElementById("editBMPanel_newFolderButton"); + newFolderButton.click(); // This will start editing. + + // Wait for editing: + await TestUtils.waitForCondition(() => !input.hidden); + + // Click the arrow to collapse the list. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_foldersExpander"), + {} + ); + + await TestUtils.waitForCondition( + () => folderTreeRow.hidden, + "Should hide the folder tree" + ); + ok(input.hidden, "Folder tree should not be broken."); + + // Click the arrow to re-show the list. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_foldersExpander"), + {} + ); + + await TestUtils.waitForCondition( + () => !folderTreeRow.hidden, + "Should re-show the folder tree" + ); + ok(input.hidden, "Folder tree should still not be broken."); + + const promiseCancel = promisePopupHidden( + document.getElementById("editBookmarkPanel") + ); + document.getElementById("editBookmarkPanelRemoveButton").click(); + await promiseCancel; + Assert.ok(!folderTree.view, "The view should have been disconnected"); +}); + +add_task(async function test_selectBookmarksMenu() { + await clickBookmarkStar(); + + // Open folder selector. + let menuList = document.getElementById("editBMPanel_folderMenuList"); + + const expectedFolder = "BookmarksMenuFolderTitle"; + const expectedGuid = PlacesUtils.bookmarks.menuGuid; + + let promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}); + await promisePopup; + + // Click the bookmarks menu item. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_bmRootItem"), + {} + ); + + await TestUtils.waitForCondition( + () => menuList.getAttribute("selectedGuid") == expectedGuid, + "Should select the menu folder item" + ); + + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + "Should have updated the menu label" + ); + + // Click the arrow to show the folder tree. + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_foldersExpander"), + {} + ); + const folderTreeRow = document.getElementById("editBMPanel_folderTreeRow"); + await BrowserTestUtils.waitForMutationCondition( + folderTreeRow, + { attributeFilter: ["hidden"] }, + () => !folderTreeRow.hidden + ); + const folderTree = document.getElementById("editBMPanel_folderTree"); + Assert.equal( + folderTree.selectedNode.bookmarkGuid, + PlacesUtils.bookmarks.virtualMenuGuid, + "Folder tree should have the correct selected guid" + ); + Assert.equal( + menuList.getAttribute("selectedGuid"), + expectedGuid, + "Menu list should have the correct selected guid" + ); + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + "Should have kept the same menu label" + ); + + // Switch back to the toolbar folder. + const { toolbarGuid } = PlacesUtils.bookmarks; + promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}); + await promisePopup; + EventUtils.synthesizeMouseAtCenter( + document.getElementById("editBMPanel_toolbarFolderItem"), + {} + ); + await TestUtils.waitForCondition( + () => menuList.getAttribute("selectedGuid") == toolbarGuid, + "Should select the toolbar folder item" + ); + + // Save the bookmark. + const promiseBookmarkAdded = + PlacesTestUtils.waitForNotification("bookmark-added"); + await hideBookmarksPanel(); + const [{ parentGuid }] = await promiseBookmarkAdded; + Assert.equal( + parentGuid, + toolbarGuid, + "Should have saved the bookmark in the correct folder." + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js new file mode 100644 index 0000000000..23eec11e1d --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_URL = "about:robots"; +StarUI._createPanelIfNeeded(); +const bookmarkPanel = document.getElementById("editBookmarkPanel"); +let folders; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + bookmarkPanel.setAttribute("animate", false); + + let oldTimeout = StarUI._autoCloseTimeout; + // Make the timeout something big, so it doesn't iteract badly with tests. + StarUI._autoCloseTimeout = 6000000; + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + StarUI._autoCloseTimeout = oldTimeout; + BrowserTestUtils.removeTab(tab); + bookmarkPanel.removeAttribute("animate"); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_newFolder() { + let newBookmarkObserver = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => url === TEST_URL) + ); + await clickBookmarkStar(); + + // Open folder selector. + document.getElementById("editBMPanel_foldersExpander").click(); + + let folderTree = document.getElementById("editBMPanel_folderTree"); + + // Create new folder. + let newFolderButton = document.getElementById("editBMPanel_newFolderButton"); + newFolderButton.click(); + + let newFolderGuid; + let newFolderObserver = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => { + for (let { guid, itemType } of events) { + newFolderGuid = guid; + if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER) { + return true; + } + } + return false; + } + ); + + let menulist = document.getElementById("editBMPanel_folderMenuList"); + + await newFolderObserver; + + // Wait for the folder to be created and for editing to start. + await TestUtils.waitForCondition( + () => folderTree.hasAttribute("editing"), + "Should be in edit mode for the new folder" + ); + + Assert.equal( + menulist.selectedItem.label, + newFolderButton.label, + "Should have the new folder selected by default" + ); + + let renameObserver = PlacesTestUtils.waitForNotification( + "bookmark-title-changed", + events => events.some(e => e.title === "f") + ); + + // Enter a new name. + EventUtils.synthesizeKey("f", {}, window); + EventUtils.synthesizeKey("VK_RETURN", {}, window); + + await renameObserver; + + await TestUtils.waitForCondition( + () => !folderTree.hasAttribute("editing"), + "Should have stopped editing the new folder" + ); + + Assert.equal( + menulist.selectedItem.label, + "f", + "Should have the new folder title" + ); + + await hideBookmarksPanel(); + await newBookmarkObserver; + let bookmark = await PlacesUtils.bookmarks.fetch({ url: TEST_URL }); + + Assert.equal( + bookmark.parentGuid, + newFolderGuid, + "The bookmark should be parented by the new folder" + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js b/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js new file mode 100644 index 0000000000..67d1406bc1 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const TEST_URL = "about:buildconfig"; + +add_task(async function test_change_title_from_BookmarkStar() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "Before Edit", + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + BrowserTestUtils.removeTab(tab); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + StarUI._createPanelIfNeeded(); + let bookmarkPanel = document.getElementById("editBookmarkPanel"); + let shownPromise = promisePopupShown(bookmarkPanel); + + let bookmarkStar = BookmarkingUI.star; + bookmarkStar.click(); + + await shownPromise; + + window.gEditItemOverlay.toggleFolderTreeVisibility(); + + let folderTree = document.getElementById("editBMPanel_folderTree"); + + // canDrop should always return false. + let bookmarkWithId = JSON.stringify( + Object.assign({ + url: "http://example.com", + title: "Fake BM", + }) + ); + + let dt = { + dropEffect: "move", + mozCursor: "auto", + mozItemCount: 1, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return this.types; + }, + mozGetDataAt(i) { + return bookmarkWithId; + }, + }; + + Assert.ok( + !folderTree.view.canDrop(1, Ci.nsITreeView.DROP_BEFORE, dt), + "Should not be able to drop a bookmark" + ); + + // User Actions should be disabled. + const userActions = [ + "cmd_undo", + "cmd_redo", + "cmd_cut", + "cmd_copy", + "cmd_paste", + "cmd_delete", + "cmd_selectAll", + // Anything starting with placesCmd_ should also be disabled. + "placesCmd_", + ]; + for (let action of userActions) { + Assert.ok( + !folderTree.view._controller.supportsCommand(action), + `${action} should be disabled for the folder tree in bookmarks properties` + ); + } + + let hiddenPromise = promisePopupHidden(bookmarkPanel); + let doneButton = document.getElementById("editBookmarkPanelDoneButton"); + doneButton.click(); + await hiddenPromise; +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js new file mode 100644 index 0000000000..d98b7477ec --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js @@ -0,0 +1,67 @@ +"use strict"; + +add_task(async function test_dialog() { + info("Bug 479348 - Properties dialog on a root should be read-only."); + await withSidebarTree("bookmarks", async function (tree) { + tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]); + Assert.ok( + !tree.controller.isCommandEnabled("placesCmd_show:info"), + "'placesCmd_show:info' on current selected node is disabled" + ); + + await withBookmarksDialog( + true, + function openDialog() { + // Even if the cmd is disabled, we can execute it regardless. + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + // Check that the dialog is read-only. + Assert.ok(dialogWin.gEditItemOverlay.readOnly, "Dialog is read-only"); + // Check that accept button is disabled + let acceptButton = dialogWin.document + .getElementById("bookmarkpropertiesdialog") + .getButton("accept"); + Assert.ok(acceptButton.disabled, "Accept button is disabled"); + + // Check that name picker is read only + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.ok(namepicker.readOnly, "Name field is read-only"); + Assert.equal( + namepicker.value, + PlacesUtils.getString("OtherBookmarksFolderTitle"), + "Node title is correct" + ); + } + ); + }); +}); + +add_task(async function test_library() { + info("Bug 479348 - Library info pane on a root should be read-only."); + let library = await promiseLibrary("UnfiledBookmarks"); + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + let PlacesOrganizer = library.PlacesOrganizer; + let tree = PlacesOrganizer._places; + tree.focus(); + Assert.ok( + !tree.controller.isCommandEnabled("placesCmd_show:info"), + "'placesCmd_show:info' on current selected node is disabled" + ); + + // Check that the pane is read-only. + Assert.ok(library.gEditItemOverlay.readOnly, "Info pane is read-only"); + + // Check that name picker is read only + let namepicker = library.document.getElementById("editBMPanel_namePicker"); + Assert.ok(namepicker.readOnly, "Name field is read-only"); + Assert.equal( + namepicker.value, + PlacesUtils.getString("OtherBookmarksFolderTitle"), + "Node title is correct" + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js new file mode 100644 index 0000000000..99da75d62f --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/** + * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar. + */ + +StarUI._createPanelIfNeeded(); +const bookmarkPanel = document.getElementById("editBookmarkPanel"); +let folders; + +async function openPopupAndSelectFolder(guid, newBookmark = false) { + await clickBookmarkStar(); + + let notificationPromise; + if (!newBookmark) { + notificationPromise = PlacesTestUtils.waitForNotification( + "bookmark-moved", + events => events.some(e => guid === e.parentGuid) + ); + } + + // Expand the folder tree. + document.getElementById("editBMPanel_foldersExpander").click(); + document.getElementById("editBMPanel_folderTree").selectItems([guid]); + + await hideBookmarksPanel(); + if (!newBookmark) { + await notificationPromise; + } +} + +async function assertRecentFolders(expectedGuids, msg) { + await clickBookmarkStar(); + + let actualGuids = []; + function getGuids() { + actualGuids = []; + const folderMenuPopup = document.getElementById( + "editBMPanel_folderMenuList" + ).menupopup; + + let separatorFound = false; + // The list of folders goes from editBMPanel_foldersSeparator to the end. + for (let child of folderMenuPopup.children) { + if (separatorFound) { + actualGuids.push(child.folderGuid); + } else if (child.id == "editBMPanel_foldersSeparator") { + separatorFound = true; + } + } + } + + // The dialog fills in the folder list asnychronously, so we might need to wait + // for that to complete. + await TestUtils.waitForCondition(() => { + getGuids(); + return actualGuids.length == expectedGuids.length; + }, `Should have opened dialog with expected recent folders for: ${msg}`); + + Assert.deepEqual(actualGuids, expectedGuids, msg); + + await hideBookmarksPanel(); + + // Give the metadata chance to be written to the database before we attempt + // to open the dialog again. + let diskGuids = []; + await TestUtils.waitForCondition(async () => { + diskGuids = await PlacesUtils.metadata.get( + PlacesUIUtils.LAST_USED_FOLDERS_META_KEY, + [] + ); + return diskGuids.length == expectedGuids.length; + }, `Should have written data to disk for: ${msg}`); + + Assert.deepEqual( + diskGuids, + expectedGuids, + `Should match the disk GUIDS for ${msg}` + ); +} + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY); + + bookmarkPanel.setAttribute("animate", false); + + let oldTimeout = StarUI._autoCloseTimeout; + // Make the timeout something big, so it doesn't iteract badly with tests. + StarUI._autoCloseTimeout = 6000000; + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:robots", + waitForStateStop: true, + }); + + folders = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Bob", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Place", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Delight", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Surprise", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Treble Bob", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Principal", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "Max Default Recent Folders", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "One Over Default Maximum Recent Folders", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + + registerCleanupFunction(async () => { + StarUI._autoCloseTimeout = oldTimeout; + BrowserTestUtils.removeTab(tab); + bookmarkPanel.removeAttribute("animate"); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY); + }); +}); + +add_task(async function test_remember_last_folder() { + await assertRecentFolders([], "Should have no recent folders to start with."); + + await openPopupAndSelectFolder(folders[0].guid, true); + + await assertRecentFolders( + [folders[0].guid], + "Should have one folder in the list." + ); +}); + +add_task(async function test_forget_oldest_folder() { + // Add some more folders. + let expectedFolders = [folders[0].guid]; + for (let i = 1; i < folders.length; i++) { + await assertRecentFolders( + expectedFolders, + "Should have only the expected folders in the list" + ); + + await openPopupAndSelectFolder(folders[i].guid); + + expectedFolders.unshift(folders[i].guid); + if (expectedFolders.length > PlacesUIUtils.maxRecentFolders) { + expectedFolders.pop(); + } + } + + await assertRecentFolders( + expectedFolders, + "Should have expired the original folder" + ); +}); + +add_task(async function test_reorder_folders() { + let expectedFolders = [ + folders[2].guid, + folders[7].guid, + folders[6].guid, + folders[5].guid, + folders[4].guid, + folders[3].guid, + folders[1].guid, + ]; + + // Take an old one and put it at the front. + await openPopupAndSelectFolder(folders[2].guid); + + await assertRecentFolders( + expectedFolders, + "Should have correctly re-ordered the list" + ); +}); + +add_task(async function test_change_max_recent_folders_pref() { + let expectedFolders = [folders[0].guid]; + + Services.prefs.setIntPref("browser.bookmarks.editDialog.maxRecentFolders", 1); + + await openPopupAndSelectFolder(folders[1].guid); + await openPopupAndSelectFolder(folders[0].guid); + + await assertRecentFolders( + expectedFolders, + "Should have only one recent folder in the bookmark edit panel" + ); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref( + "browser.bookmarks.editDialog.maxRecentFolders" + ); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js b/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js new file mode 100644 index 0000000000..621ea19bb9 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test to ensure that on "mousedown" in Toolbar we set Speculative Connection + */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const sandbox = sinon.createSandbox(); +let spy = sandbox + .stub(PlacesUIUtils, "setupSpeculativeConnection") + .returns(Promise.resolve()); + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + let toolbar = document.getElementById("PersonalToolbar"); + ok(toolbar, "PersonalToolbar should not be null"); + + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + sandbox.restore(); + }); +}); + +add_task(async function checkToolbarSpeculativeConnection() { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "https://example.com/", + title: "Bookmark 1", + }); + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + info("Synthesize mousedown on selected bookmark"); + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + type: "mousedown", + }); + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + type: "mouseup", + }); + + Assert.ok(spy.called, "Speculative connection for Toolbar called"); + sandbox.restore(); +}); + +add_task(async function checkMenuSpeculativeConnection() { + await PlacesUtils.bookmarks.eraseEverything(); + + info("Placing a Menu widget"); + let origBMBlocation = CustomizableUI.getPlacementOfWidget( + "bookmarks-menu-button" + ); + // Ensure BMB is available in UI. + if (!origBMBlocation) { + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR + ); + } + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + // if BMB was not originally in UI, remove it. + if (!origBMBlocation) { + CustomizableUI.removeWidgetFromArea("bookmarks-menu-button"); + } + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://example.com/", + title: "Bookmark 2", + }); + + // Test Bookmarks Menu Button + let BMB = document.getElementById("bookmarks-menu-button"); + let BMBpopup = document.getElementById("BMB_bookmarksPopup"); + let promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(BMB, {}); + await promiseEvent; + info("Popupshown on Bookmarks-Menu-Button"); + + let menuBookmark = [...BMBpopup.children].find( + node => node.label == "Bookmark 2" + ); + + EventUtils.synthesizeMouseAtCenter(menuBookmark, { + type: "mousedown", + }); + EventUtils.synthesizeMouseAtCenter(menuBookmark, { + type: "mouseup", + }); + + Assert.ok(spy.called, "Speculative connection for Menu Button called"); + sandbox.restore(); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js b/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js new file mode 100644 index 0000000000..1ba6f56949 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +add_task(async function () { + let mainFolder = await PlacesUtils.bookmarks.insert({ + title: "mainFolder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await withSidebarTree("bookmarks", async function (tree) { + await PlacesUtils.bookmarks.insertTree({ + guid: mainFolder.guid, + children: [ + { + title: "firstFolder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "secondFolder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + tree.selectItems([mainFolder.guid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + let firstFolder = tree.selectedNode.getChild(0); + + tree.selectNode(firstFolder); + info("Synthesize click on selected node to open it."); + synthesizeClickOnSelectedTreeCell(tree); + info(`Get the hashed uri starts with "place:" and hash key&value pairs.`); + let hashedKey = PlacesUIUtils.obfuscateUrlForXulStore(firstFolder.uri); + + let docUrl = "chrome://browser/content/places/bookmarksSidebar.xhtml"; + + let value = Services.xulStore.getValue(docUrl, hashedKey, "open"); + + Assert.ok(hashedKey.startsWith("place:"), "Sanity check the hashed key"); + Assert.equal(value, "true", "Check the expected xulstore value"); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_add_tags.js b/browser/components/places/tests/browser/browser_bookmark_add_tags.js new file mode 100644 index 0000000000..b031e1d219 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar. + */ +let bookmarkPanel; +let bookmarkStar; + +async function clickBookmarkStar() { + let shownPromise = promisePopupShown(bookmarkPanel); + bookmarkStar.click(); + await shownPromise; +} + +async function hideBookmarksPanel(callback) { + let hiddenPromise = promisePopupHidden(bookmarkPanel); + callback(); + await hiddenPromise; +} + +registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_add_bookmark_tags_from_bookmarkProperties() { + const TEST_URL = "about:robots"; + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "about:buildconfig", + title: "Bookmark Title", + }); + + PlacesUtils.tagging.tagURI(makeURI("about:buildconfig"), ["tag0"]); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + win.StarUI._createPanelIfNeeded(); + win.StarUI._autoCloseTimeout = 1000; + bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + bookmarkStar = win.BookmarkingUI.star; + + // Cleanup. + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.closeWindow(win); + }); + + let bookmarkPanelTitle = win.document.getElementById( + "editBookmarkPanelTitle" + ); + + // The bookmarks panel is expected to auto-close after this step. + let promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => !url || url == TEST_URL) + ); + await hideBookmarksPanel(async () => { + // Click the bookmark star to bookmark the page. + await clickBookmarkStar(); + await TestUtils.waitForCondition( + () => + win.document.l10n.getAttributes(bookmarkPanelTitle).id === + "bookmarks-add-bookmark", + "Bookmark title is correct" + ); + Assert.equal( + bookmarkStar.getAttribute("starred"), + "true", + "Page is starred" + ); + }); + await promiseNotification; + + // Click the bookmark star again to add tags. + await clickBookmarkStar(); + promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + await TestUtils.waitForCondition( + () => + win.document.l10n.getAttributes(bookmarkPanelTitle).id === + "bookmarks-edit-bookmark", + "Bookmark title is correct" + ); + await fillBookmarkTextField("editBMPanel_tagsField", "tag1", win); + const doneButton = win.document.getElementById("editBookmarkPanelDoneButton"); + await hideBookmarksPanel(() => doneButton.click()); + await promiseNotification; + Assert.equal( + PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length, + 1, + "Found the right number of tags" + ); + Assert.deepEqual( + PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)), + ["tag1"] + ); + + // Click the bookmark star again, add more tags. + await clickBookmarkStar(); + promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + await fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2, tag3", win); + await hideBookmarksPanel(() => doneButton.click()); + await promiseNotification; + + const bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => + bookmarks.push(bm) + ); + Assert.equal(bookmarks.length, 1, "Only one bookmark should exist"); + Assert.equal( + PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length, + 3, + "Found the right number of tags" + ); + Assert.deepEqual( + PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)), + ["tag1", "tag2", "tag3"] + ); + + // Cleanup. + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_add_bookmark_tags_from_library() { + const uri = "http://example.com/"; + + // Add a bookmark. + await PlacesUtils.bookmarks.insert({ + url: uri, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + // Open the Library on "UnfiledBookmarks". + let library = await promiseLibrary("UnfiledBookmarks"); + + // Cleanup. + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + + let bookmarkNode = library.ContentTree.view.selectedNode; + Assert.equal( + bookmarkNode.uri, + "http://example.com/", + "Found the expected bookmark" + ); + + // Add a tag to the bookmark. + fillBookmarkTextField("editBMPanel_tagsField", "tag1", library); + + await TestUtils.waitForCondition( + () => bookmarkNode.tags === "tag1", + "Node tag is correct" + ); + + // Add a new tag to the bookmark. + fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2", library); + + await TestUtils.waitForCondition( + () => bookmarkNode.tags === "tag1,tag2", + "Node tag is correct" + ); + + // Check the tag change has been completed. + let tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(uri)); + Assert.equal(tags.length, 2, "Found the right number of tags"); + Assert.deepEqual(tags, ["tag1", "tag2"], "Found the expected tags"); + + await promiseLibraryClosed(library); + // Cleanup. + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_add_bookmark_tags_from_sidebar() { + const TEST_URL = "about:buildconfig"; + + let bookmarks = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "Bookmark Title", + }); + + await withSidebarTree("bookmarks", async function (tree) { + tree.selectItems([bookmarks.guid]); + // Add one tag. + await addTags(["tag1"], tree, ["tag1"]); + // Add 2 more tags. + await addTags(["tag2", "tag3"], tree, ["tag1", "tag2", "tag3"]); + }); + + async function addTags(tagValue, tree, expected) { + await withBookmarksDialog( + false, + function openPropertiesDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + PlacesUtils.tagging.tagURI(makeURI(TEST_URL), tagValue); + let tags = PlacesUtils.tagging.getTagsForURI( + Services.io.newURI(TEST_URL) + ); + + Assert.deepEqual(tags, expected, "Tags field is correctly populated"); + + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + } + ); + } + + // Cleanup. + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_all_tabs.js b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js new file mode 100644 index 0000000000..2852bf4019 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js @@ -0,0 +1,46 @@ +/** + * Test for Bug 446171 - Name field of bookmarks saved via 'Bookmark All Tabs' + * has '(null)' value if history is disabled or just in private browsing mode + */ +"use strict"; + +add_task(async function () { + const BASE_URL = + "http://example.org/browser/browser/components/places/tests/browser/"; + const TEST_PAGES = [ + BASE_URL + "bookmark_dummy_1.html", + BASE_URL + "bookmark_dummy_2.html", + BASE_URL + "bookmark_dummy_1.html", + ]; + + function promiseAddTab(url) { + return BrowserTestUtils.openNewForegroundTab(gBrowser, url); + } + + let tabs = await Promise.all(TEST_PAGES.map(promiseAddTab)); + + let URIs = PlacesCommandHook.uniqueCurrentPages; + is(URIs.length, 3, "Only unique pages are returned"); + + Assert.deepEqual( + URIs.map(URI => URI.uri.spec), + [ + "about:blank", + BASE_URL + "bookmark_dummy_1.html", + BASE_URL + "bookmark_dummy_2.html", + ], + "Correct URIs are returned" + ); + + Assert.deepEqual( + URIs.map(URI => URI.title), + ["New Tab", "Bookmark Dummy 1", "Bookmark Dummy 2"], + "Correct titles are returned" + ); + + registerCleanupFunction(async function () { + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js new file mode 100644 index 0000000000..8b954a8469 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests bookmarks backup export/import as JSON file. + */ + +const BASE_URL = "http://example.com/"; + +const PLACES = [ + { + guid: PlacesUtils.bookmarks.menuGuid, + prefix: "In Menu", + total: 5, + }, + { + guid: PlacesUtils.bookmarks.toolbarGuid, + prefix: "In Toolbar", + total: 7, + }, + { + guid: PlacesUtils.bookmarks.unfiledGuid, + prefix: "In Other", + total: 8, + }, +]; + +var importExportPicker, saveDir, actualBookmarks; + +async function generateTestBookmarks() { + actualBookmarks = []; + for (let place of PLACES) { + let currentPlaceChildren = []; + for (let i = 1; i <= place.total; i++) { + currentPlaceChildren.push({ + url: `${BASE_URL}${i}`, + title: `${place.prefix} Bookmark: ${i}`, + }); + } + await PlacesUtils.bookmarks.insertTree({ + guid: place.guid, + children: currentPlaceChildren, + }); + actualBookmarks = actualBookmarks.concat(currentPlaceChildren); + } +} + +async function validateImportedBookmarksByParent( + parentGuid, + expectedChildrenTotal +) { + let currentPlace = PLACES.filter(elem => { + return elem.guid === parentGuid.toString(); + })[0]; + + let bookmarksTree = await PlacesUtils.promiseBookmarksTree(parentGuid); + + Assert.equal( + bookmarksTree.children.length, + expectedChildrenTotal, + `Imported bookmarks length should be ${expectedChildrenTotal}` + ); + + for (let importedBookmark of bookmarksTree.children) { + Assert.equal( + importedBookmark.type, + PlacesUtils.TYPE_X_MOZ_PLACE, + `Exported bookmarks should be of type bookmark` + ); + + let doesTitleContain = importedBookmark.title + .toString() + .includes(`${currentPlace.prefix} Bookmark`); + Assert.equal( + doesTitleContain, + true, + `Bookmark title should contain text: ${currentPlace.prefix} Bookmark` + ); + + let doesUriContains = importedBookmark.uri.toString().includes(BASE_URL); + Assert.equal(doesUriContains, true, "Bookmark uri should contain base url"); + } +} + +async function validateImportedBookmarks(fromPlaces) { + for (let i = 0; i < fromPlaces.length; i++) { + let parentContainer = fromPlaces[i]; + await validateImportedBookmarksByParent( + parentContainer.guid, + parentContainer.total + ); + } +} + +async function promiseImportExport(aWindow) { + saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("temp-bookmarks-export"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + importExportPicker.displayDirectory = saveDir; + + return new Promise(resolve => { + importExportPicker.showCallback = async () => { + let fileName = "bookmarks-backup.json"; + let destFile = saveDir.clone(); + destFile.append(fileName); + importExportPicker.setFiles([destFile]); + resolve(destFile); + }; + }); +} + +add_setup(async function () { + await promisePlacesInitComplete(); + await PlacesUtils.bookmarks.eraseEverything(); + await generateTestBookmarks(); + importExportPicker = SpecialPowers.MockFilePicker; + importExportPicker.init(window); + + registerCleanupFunction(async () => { + importExportPicker.cleanup(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +async function showMaintenancePopup(libraryWindow) { + let button = libraryWindow.document.getElementById("maintenanceButton"); + let popup = libraryWindow.document.getElementById("maintenanceButtonPopup"); + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + info("Clicking maintenance menu"); + + button.openMenu(true); + + await shown; + info("Maintenance popup shown"); + return popup; +} + +add_task(async function test_export_json() { + let libraryWindow = await promiseLibrary(); + let popup = await showMaintenancePopup(libraryWindow); + let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + + info("Activating #backupBookmarks"); + + let backupPromise = promiseImportExport(); + + popup.activateItem(popup.querySelector("#backupBookmarks")); + await hidden; + + info("Popup hidden"); + + let backupFile = await backupPromise; + await TestUtils.waitForCondition( + backupFile.exists, + "Backup file should exist" + ); + await promiseLibraryClosed(libraryWindow); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +async function showFileRestorePopup(libraryWindow) { + let parentPopup = await showMaintenancePopup(libraryWindow); + let popup = parentPopup.querySelector("#fileRestorePopup"); + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + parentPopup.querySelector("#fileRestoreMenu").openMenu(true); + await shown; + return popup; +} + +add_task(async function test_import_json() { + let libraryWindow = await promiseLibrary(); + let popup = await showFileRestorePopup(libraryWindow); + + let backupPromise = promiseImportExport(); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + + let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + popup.activateItem(popup.querySelector("#restoreFromFile")); + await hidden; + + await backupPromise; + await dialogPromise; + + let restored = 0; + let promiseBookmarksRestored = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(() => ++restored == actualBookmarks.length) + ); + + await promiseBookmarksRestored; + await validateImportedBookmarks(PLACES); + await promiseLibraryClosed(libraryWindow); + + registerCleanupFunction(async () => { + if (saveDir) { + saveDir.remove(true); + } + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_change_location.js b/browser/components/places/tests/browser/browser_bookmark_change_location.js new file mode 100644 index 0000000000..3a82b67a93 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_change_location.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the bookmark location (url) can be changed from the toolbar and the sidebar. + */ +"use strict"; + +const TEST_URL = "about:buildconfig"; +const TEST_URL2 = "about:credits"; +const TEST_URL3 = "about:config"; + +// Setup. +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.bookmarks.visibility", "always"]], + }); + + // The following initialization code is necessary to avoid a frequent + // intermittent failure in verify-fission where, due to timings, we may or + // may not import default bookmarks. We also want to avoid the empty toolbar + // placeholder shifting stuff around. + info("Ensure Places init is complete"); + let placesInitCompleteObserved = TestUtils.topicObserved( + "places-browser-init-complete" + ); + Cc["@mozilla.org/browser/browserglue;1"] + .getService(Ci.nsIObserver) + .observe(null, "browser-glue-test", "places-browser-init-complete"); + await placesInitCompleteObserved; + info("Add a bookmark to avoid showing the empty toolbar placeholder."); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "initial", + url: TEST_URL, + }); + + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + if (wasCollapsed) { + info("Show the bookmarks toolbar"); + await promiseSetToolbarVisibility(toolbar, true); + info("Ensure toolbar visibility was updated"); + await BrowserTestUtils.waitForEvent( + toolbar, + "BookmarksToolbarVisibilityUpdated" + ); + } + + // Cleanup. + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_change_location_from_Toolbar() { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "", + url: TEST_URL, + }); + + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + await withBookmarksDialog( + false, + async function openPropertiesDialog() { + let placesContext = document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await promisePopup; + + let properties = document.getElementById( + "placesContext_show_bookmark:info" + ); + placesContext.activateItem(properties); + }, + async function test(dialogWin) { + // Check the initial location. + let locationPicker = dialogWin.document.getElementById( + "editBMPanel_locationField" + ); + Assert.equal( + locationPicker.value, + TEST_URL, + "EditBookmark: The current location is the expected one." + ); + + // To check whether the lastModified field will be updated correctly. + let lastModified = _getLastModified(toolbarBookmark.guid); + + // Update the "location" field. + fillBookmarkTextField( + "editBMPanel_locationField", + TEST_URL2, + dialogWin, + false + ); + + locationPicker.blur(); + + Assert.equal( + locationPicker.value, + TEST_URL2, + "EditBookmark: The changed location is the expected one." + ); + + locationPicker.focus(); + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + + await TestUtils.waitForCondition( + () => _getLastModified(toolbarBookmark.guid) > lastModified, + "EditBookmark: The lastModified will be greater than before updating." + ); + } + ); + + let updatedBm = await PlacesUtils.bookmarks.fetch(toolbarBookmark.guid); + Assert.equal( + updatedBm.url, + TEST_URL2, + "EditBookmark: Should have updated the bookmark location in the database." + ); +}); + +add_task(async function test_change_location_from_Sidebar() { + let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL2 }); + + await withSidebarTree("bookmarks", async function (tree) { + tree.selectItems([bm.guid]); + + await withBookmarksDialog( + false, + function openPropertiesDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + // Check the initial location. + let locationPicker = dialogWin.document.getElementById( + "editBMPanel_locationField" + ); + Assert.equal( + locationPicker.value, + TEST_URL2, + "Sidebar - EditBookmark: The current location is the expected one." + ); + + // To check whether the lastModified field will be updated correctly. + let lastModified = _getLastModified(bm.guid); + + // Update the "location" field. + fillBookmarkTextField( + "editBMPanel_locationField", + TEST_URL3, + dialogWin, + false + ); + + Assert.equal( + locationPicker.value, + TEST_URL3, + "Sidebar - EditBookmark: The location is changed in dialog for prefered one." + ); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + + await TestUtils.waitForCondition( + () => _getLastModified(bm.guid) > lastModified, + "Sidebar - EditBookmark: The lastModified will be greater than before updating." + ); + } + ); + + let updatedBm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal( + updatedBm.url, + TEST_URL3, + "Sidebar - EditBookmark: Should have updated the bookmark location in the database." + ); + }); +}); + +function _getLastModified(guid) { + const toolbarNode = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ).root; + + try { + for (let i = 0; i < toolbarNode.childCount; i++) { + const node = toolbarNode.getChild(i); + if (node.bookmarkGuid === guid) { + return node.lastModified; + } + } + + throw new Error(`Node for ${guid} was not found`); + } finally { + toolbarNode.containerOpen = false; + } +} diff --git a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js new file mode 100644 index 0000000000..ac9120d3d6 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js @@ -0,0 +1,798 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test removing bookmarks from the Bookmarks Toolbar and Library. + */ +const SECOND_BOOKMARK_TITLE = "Second Bookmark Title"; +const bookmarksInfo = [ + { + title: "firefox", + url: "http://example.com", + }, + { + title: "rules", + url: "http://example.com/2", + }, + { + title: "yo", + url: "http://example.com/2", + }, +]; +const TEST_URL = "about:mozilla"; + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "userContextEnabled", + "privacy.userContext.enabled" +); + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +let OptionItemExists = (elementId, doc = document) => { + let optionItem = doc.getElementById(elementId); + + Assert.ok(optionItem, `Context menu contains the menuitem ${elementId}`); + Assert.ok( + BrowserTestUtils.isVisible(optionItem), + `Context menu option ${elementId} is visible` + ); +}; + +let OptionsMatchExpected = (contextMenu, expectedOptionItems) => { + let idList = []; + for (let elem of contextMenu.children) { + if ( + BrowserTestUtils.isVisible(elem) && + elem.localName !== "menuseparator" + ) { + idList.push(elem.id); + } + } + + Assert.deepEqual( + idList.sort(), + expectedOptionItems.sort(), + "Content is the same across both lists" + ); +}; + +let checkContextMenu = async (cbfunc, optionItems, doc = document) => { + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: SECOND_BOOKMARK_TITLE, + url: TEST_URL, + }); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + + // Open and check the context menu twice, once with + // `browser.tabs.loadBookmarksInTabs` set to true and again with it set to + // false. + for (let loadBookmarksInNewTab of [true, false]) { + info( + `Running checkContextMenu: ` + JSON.stringify({ loadBookmarksInNewTab }) + ); + + Services.prefs.setBoolPref( + "browser.tabs.loadBookmarksInTabs", + loadBookmarksInNewTab + ); + + // When `loadBookmarksInTabs` is true, the usual placesContext_open:newtab + // item is hidden and placesContext_open is shown. The tasks in this test + // assume that `loadBookmarksInTabs` is false, so when a caller expects + // placesContext_open:newtab to appear but not placesContext_open, add it to + // the list of expected items when the pref is set. + let expectedOptionItems = [...optionItems]; + if ( + loadBookmarksInNewTab && + optionItems.includes("placesContext_open:newtab") && + !optionItems.includes("placesContext_open") + ) { + expectedOptionItems.push("placesContext_open"); + } + + // The caller is responsible for opening the menu, via `cbfunc()`. + let contextMenu = await cbfunc(bookmark); + + for (let item of expectedOptionItems) { + OptionItemExists(item, doc); + } + + OptionsMatchExpected(contextMenu, expectedOptionItems); + + // Check the "default" attributes on placesContext_open and + // placesContext_open:newtab. + if (expectedOptionItems.includes("placesContext_open")) { + Assert.equal( + doc.getElementById("placesContext_open").getAttribute("default"), + loadBookmarksInNewTab ? "" : "true", + `placesContext_open has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}` + ); + } + if (expectedOptionItems.includes("placesContext_open:newtab")) { + Assert.equal( + doc.getElementById("placesContext_open:newtab").getAttribute("default"), + loadBookmarksInNewTab ? "true" : "", + `placesContext_open:newtab has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}` + ); + } + + contextMenu.hidePopup(); + } + + Services.prefs.clearUserPref("browser.tabs.loadBookmarksInTabs"); + await PlacesUtils.bookmarks.eraseEverything(); +}; + +add_task(async function test_bookmark_contextmenu_contents() { + let optionItems = [ + "placesContext_open:newtab", + "placesContext_open:newcontainertab", + "placesContext_open:newwindow", + "placesContext_open:newprivatewindow", + "placesContext_show_bookmark:info", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + "placesContext_showAllBookmarks", + "toggle_PersonalToolbar", + "show-other-bookmarks_PersonalToolbar", + ]; + if (!userContextEnabled) { + optionItems.splice( + optionItems.indexOf("placesContext_open:newcontainertab"), + 1 + ); + } + + await checkContextMenu(async function () { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Bookmark Title", + url: TEST_URL, + }); + + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + let contextMenu = document.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await popupShownPromise; + return contextMenu; + }, optionItems); + + let tabs = []; + let contextMenuOnContent; + + await checkContextMenu(async function () { + info("Check context menu after opening context menu on content"); + const toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Bookmark Title", + url: TEST_URL, + }); + + info("Open context menu on about:config"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:config" + ); + tabs.push(tab); + contextMenuOnContent = document.getElementById("contentAreaContextMenu"); + const popupShownPromiseOnContent = BrowserTestUtils.waitForEvent( + contextMenuOnContent, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(tab.linkedBrowser, { + button: 2, + type: "contextmenu", + }); + await popupShownPromiseOnContent; + contextMenuOnContent.hidePopup(); + + info("Check context menu on bookmark"); + const toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + const contextMenu = document.getElementById("placesContext"); + const popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await popupShownPromise; + + return contextMenu; + }, optionItems); + + // We need to do a thorough cleanup to avoid leaking the window of + // 'about:config'. + for (let tab of tabs) { + const tabClosed = BrowserTestUtils.waitForTabClosing(tab); + BrowserTestUtils.removeTab(tab); + await tabClosed; + } +}); + +add_task(async function test_empty_contextmenu_contents() { + let optionItems = [ + "placesContext_openBookmarkContainer:tabs", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + "placesContext_paste", + "placesContext_showAllBookmarks", + "toggle_PersonalToolbar", + "show-other-bookmarks_PersonalToolbar", + ]; + + await checkContextMenu(async function () { + let contextMenu = document.getElementById("placesContext"); + let toolbar = document.querySelector("#PlacesToolbarItems"); + let openToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + + // Use the end of the toolbar because the beginning (and even middle, on + // some resolutions) might be occluded by the empty toolbar message, which + // has a different context menu. + let bounds = toolbar.getBoundingClientRect(); + EventUtils.synthesizeMouse(toolbar, bounds.width - 5, 5, { + type: "contextmenu", + }); + + await openToolbarContextMenuPromise; + return contextMenu; + }, optionItems); +}); + +add_task(async function test_separator_contextmenu_contents() { + let optionItems = [ + "placesContext_delete", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + "placesContext_showAllBookmarks", + "toggle_PersonalToolbar", + "show-other-bookmarks_PersonalToolbar", + ]; + + await checkContextMenu(async function () { + let sep = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + let toolbarNode = getToolbarNodeForItemGuid(sep.guid); + let contextMenu = document.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await popupShownPromise; + return contextMenu; + }, optionItems); +}); + +add_task(async function test_folder_contextmenu_contents() { + let optionItems = [ + "placesContext_openBookmarkContainer:tabs", + "placesContext_show_folder:info", + "placesContext_deleteFolder", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + "placesContext_sortBy:name", + "placesContext_showAllBookmarks", + "toggle_PersonalToolbar", + "show-other-bookmarks_PersonalToolbar", + ]; + + await checkContextMenu(async function () { + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + let toolbarNode = getToolbarNodeForItemGuid(folder.guid); + let contextMenu = document.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await popupShownPromise; + return contextMenu; + }, optionItems); +}); + +add_task(async function test_sidebar_folder_contextmenu_contents() { + let optionItems = [ + "placesContext_show_folder:info", + "placesContext_deleteFolder", + "placesContext_cut", + "placesContext_copy", + "placesContext_openBookmarkContainer:tabs", + "placesContext_sortBy:name", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + tree.selectItems([folder.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_multiple_folders_contextmenu_contents() { + let optionItems = [ + "placesContext_show_folder:info", + "placesContext_deleteFolder", + "placesContext_cut", + "placesContext_copy", + "placesContext_sortBy:name", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "folder 1", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let folder2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "folder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + tree.selectItems([folder1.guid, folder2.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_bookmark_contextmenu_contents() { + let optionItems = [ + "placesContext_open:newtab", + "placesContext_open:newcontainertab", + "placesContext_open:newwindow", + "placesContext_open:newprivatewindow", + "placesContext_show_bookmark:info", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + if (!userContextEnabled) { + optionItems.splice( + optionItems.indexOf("placesContext_open:newcontainertab"), + 1 + ); + } + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + tree.selectItems([bookmark.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_bookmark_search_contextmenu_contents() { + let optionItems = [ + "placesContext_open:newtab", + "placesContext_open:newcontainertab", + "placesContext_open:newwindow", + "placesContext_open:newprivatewindow", + "placesContext_showInFolder", + "placesContext_show_bookmark:info", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + ]; + if (!userContextEnabled) { + optionItems.splice( + optionItems.indexOf("placesContext_open:newcontainertab"), + 1 + ); + } + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + info("Checking bookmark sidebar menu contents in search context"); + // Perform a search first + let searchBox = + SidebarUI.browser.contentDocument.getElementById("search-box"); + searchBox.value = SECOND_BOOKMARK_TITLE; + searchBox.doCommand(); + tree.selectItems([bookmark.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_library_bookmark_contextmenu_contents() { + let optionItems = [ + "placesContext_open", + "placesContext_open:newtab", + "placesContext_open:newcontainertab", + "placesContext_open:newwindow", + "placesContext_open:newprivatewindow", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + if (!userContextEnabled) { + optionItems.splice( + optionItems.indexOf("placesContext_open:newcontainertab"), + 1 + ); + } + + await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => { + await checkContextMenu( + async bookmark => { + let contextMenu = right.ownerDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + right.selectItems([bookmark.guid]); + synthesizeClickOnSelectedTreeCell(right, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + right.ownerDocument + ); + }); +}); + +add_task(async function test_library_bookmark_search_contextmenu_contents() { + let optionItems = [ + "placesContext_open", + "placesContext_open:newtab", + "placesContext_open:newcontainertab", + "placesContext_open:newwindow", + "placesContext_open:newprivatewindow", + "placesContext_showInFolder", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + ]; + if (!userContextEnabled) { + optionItems.splice( + optionItems.indexOf("placesContext_open:newcontainertab"), + 1 + ); + } + + await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => { + await checkContextMenu( + async bookmark => { + info("Checking bookmark library menu contents in search context"); + // Perform a search first + let searchBox = right.ownerDocument.getElementById("searchFilter"); + searchBox.value = SECOND_BOOKMARK_TITLE; + searchBox.doCommand(); + + let contextMenu = right.ownerDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + right.selectItems([bookmark.guid]); + synthesizeClickOnSelectedTreeCell(right, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + right.ownerDocument + ); + }); +}); + +add_task(async function test_sidebar_mixedselection_contextmenu_contents() { + let optionItems = [ + "placesContext_delete", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + tree.selectItems([bookmark.guid, folder.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_multiple_bookmarks_contextmenu_contents() { + let optionItems = [ + "placesContext_openBookmarkLinks:tabs", + "placesContext_show_bookmark:info", + "placesContext_deleteBookmark", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + let bookmark2 = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + tree.selectItems([bookmark.guid, bookmark2.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_multiple_links_contextmenu_contents() { + let optionItems = [ + "placesContext_openLinks:tabs", + "placesContext_delete_history", + "placesContext_copy", + "placesContext_createBookmark", + ]; + + await withSidebarTree("history", async tree => { + await checkContextMenu( + async bookmark => { + await PlacesTestUtils.addVisits([ + "http://example-1.com/", + "http://example-2.com/", + ]); + // Sort by last visited. + tree.ownerDocument.getElementById("bylastvisited").doCommand(); + tree.selectAll(); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_sidebar_mixed_bookmarks_contextmenu_contents() { + let optionItems = [ + "placesContext_delete", + "placesContext_cut", + "placesContext_copy", + "placesContext_paste_group", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + ]; + + await withSidebarTree("bookmarks", async tree => { + await checkContextMenu( + async bookmark => { + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + tree.selectItems([bookmark.guid, folder.guid]); + + let contextMenu = + SidebarUI.browser.contentDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" }); + await popupShownPromise; + return contextMenu; + }, + optionItems, + SidebarUI.browser.contentDocument + ); + }); +}); + +add_task(async function test_library_noselection_contextmenu_contents() { + let optionItems = [ + "placesContext_openBookmarkContainer:tabs", + "placesContext_new:bookmark", + "placesContext_new:folder", + "placesContext_new:separator", + "placesContext_paste", + ]; + + await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => { + await checkContextMenu( + async bookmark => { + let contextMenu = right.ownerDocument.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + right.selectItems([]); + EventUtils.synthesizeMouseAtCenter( + right.body, + { type: "contextmenu" }, + right.ownerGlobal + ); + await popupShownPromise; + return contextMenu; + }, + optionItems, + right.ownerDocument + ); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js b/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js new file mode 100644 index 0000000000..71b947f7ac --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + let mainFolder = await PlacesUtils.bookmarks.insert({ + title: "mainFolder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await withSidebarTree("bookmarks", async function (tree) { + const selectedNodeComparator = { + equalTitle: itemNode => { + Assert.equal( + tree.selectedNode.title, + itemNode.title, + "Select expected title" + ); + }, + equalNode: itemNode => { + Assert.equal( + tree.selectedNode.bookmarkGuid, + itemNode.guid, + "Selected the expected node" + ); + }, + equalType: itemType => { + Assert.equal(tree.selectedNode.type, itemType, "Correct type"); + }, + + equalChildCount: childrenAmount => { + Assert.equal( + tree.selectedNode.childCount, + childrenAmount, + `${childrenAmount} children` + ); + }, + }; + let urlType = Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; + + info("Create tree of: folderA => subFolderA => 3 bookmarkItems"); + await PlacesUtils.bookmarks.insertTree({ + guid: mainFolder.guid, + children: [ + { + title: "FolderA", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "subFolderA", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "firstBM", + url: "http://example.com/1", + }, + { + title: "secondBM", + url: "http://example.com/2", + }, + { + title: "thirdBM", + url: "http://example.com/3", + }, + ], + }, + ], + }, + ], + }); + + info("Sanity check folderA, subFolderA, bookmarkItems"); + tree.selectItems([mainFolder.guid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(mainFolder); + selectedNodeComparator.equalChildCount(1); + + let sourceFolder = tree.selectedNode.getChild(0); + tree.selectNode(sourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(sourceFolder); + selectedNodeComparator.equalChildCount(1); + + let subSourceFolder = tree.selectedNode.getChild(0); + tree.selectNode(subSourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(subSourceFolder); + selectedNodeComparator.equalChildCount(3); + + let bm_2 = tree.selectedNode.getChild(1); + tree.selectNode(bm_2); + selectedNodeComparator.equalTitle(bm_2); + + info( + "Copy folder tree from sourceFolder (folderA, subFolderA, bookmarkItems)" + ); + tree.selectNode(sourceFolder); + await promiseClipboard(() => { + tree.controller.copy(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + tree.selectItems([mainFolder.guid]); + + info("Paste copy of folderA"); + await tree.controller.paste(); + + info("Sanity check copy/paste operation - mainFolder has 2 children"); + tree.selectItems([mainFolder.guid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalChildCount(2); + + info("Sanity check copy of folderA"); + let copySourceFolder = tree.selectedNode.getChild(1); + tree.selectNode(copySourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(copySourceFolder); + selectedNodeComparator.equalChildCount(1); + + info("Sanity check copy subFolderA"); + let copySubSourceFolder = tree.selectedNode.getChild(0); + tree.selectNode(copySubSourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(copySubSourceFolder); + selectedNodeComparator.equalChildCount(3); + + info("Sanity check copy BookmarkItem"); + let copyBm_1 = tree.selectedNode.getChild(0); + tree.selectNode(copyBm_1); + selectedNodeComparator.equalTitle(copyBm_1); + + info("Undo copy operation"); + await PlacesTransactions.undo(); + tree.selectItems([mainFolder.guid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + + info("Sanity check undo operation - mainFolder has 1 child"); + selectedNodeComparator.equalChildCount(1); + + info("Redo copy operation"); + await PlacesTransactions.redo(); + tree.selectItems([mainFolder.guid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + + info("Sanity check redo operation - mainFolder has 2 children"); + selectedNodeComparator.equalChildCount(2); + + info("Sanity check copy of folderA"); + copySourceFolder = tree.selectedNode.getChild(1); + tree.selectNode(copySourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalChildCount(1); + + info("Sanity check copy subFolderA"); + copySubSourceFolder = tree.selectedNode.getChild(0); + tree.selectNode(copySubSourceFolder); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + selectedNodeComparator.equalTitle(copySubSourceFolder); + selectedNodeComparator.equalChildCount(3); + + info("Sanity check copy BookmarkItem"); + let copyBm_2 = tree.selectedNode.getChild(1); + tree.selectNode(copyBm_2); + selectedNodeComparator.equalTitle(copyBm_2); + selectedNodeComparator.equalType(urlType); + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js new file mode 100644 index 0000000000..d981aa4713 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js @@ -0,0 +1,139 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + let root = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await withSidebarTree("bookmarks", async function (tree) { + info("Test a regular folder"); + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + tree.selectItems([folder.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + folder.guid, + "Selected the expected node" + ); + Assert.equal(tree.selectedNode.type, 6, "node is a folder"); + Assert.ok( + tree.controller.canMoveNode(tree.selectedNode), + "can move regular folder node" + ); + + info("Test a folder shortcut"); + let shortcut = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "bar", + url: `place:parent=${folder.guid}`, + }); + tree.selectItems([shortcut.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + shortcut.guid, + "Selected the expected node" + ); + Assert.equal(tree.selectedNode.type, 9, "node is a folder shortcut"); + Assert.equal( + PlacesUtils.getConcreteItemGuid(tree.selectedNode), + folder.guid, + "shortcut node guid and concrete guid match" + ); + Assert.ok( + tree.controller.canMoveNode(tree.selectedNode), + "can move folder shortcut node" + ); + + info("Test a query"); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "", + url: "http://foo.com", + }); + tree.selectItems([bookmark.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + bookmark.guid, + "Selected the expected node" + ); + let query = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "bar", + url: `place:terms=foo`, + }); + tree.selectItems([query.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + query.guid, + "Selected the expected node" + ); + Assert.ok( + tree.controller.canMoveNode(tree.selectedNode), + "can move query node" + ); + + info("Test a tag container"); + PlacesUtils.tagging.tagURI(bookmark.url.URI, ["bar"]); + // Add the tags root query. + let tagsQuery = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "", + url: "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT, + }); + tree.selectItems([tagsQuery.guid]); + PlacesUtils.asQuery(tree.selectedNode).containerOpen = true; + Assert.equal(tree.selectedNode.childCount, 1, "has tags"); + let tagNode = tree.selectedNode.getChild(0); + Assert.ok( + !tree.controller.canMoveNode(tagNode), + "should not be able to move tag container node" + ); + tree.selectedNode.containerOpen = false; + + info( + "Test that special folders and cannot be moved but other shortcuts can." + ); + let roots = [ + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.toolbarGuid, + ]; + + for (let guid of roots) { + tree.selectItems([guid]); + Assert.ok( + !tree.controller.canMoveNode(tree.selectedNode), + "shouldn't be able to move default shortcuts to roots" + ); + let s = await PlacesUtils.bookmarks.insert({ + parentGuid: root.guid, + title: "bar", + url: `place:parent=${guid}`, + }); + tree.selectItems([s.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + s.guid, + "Selected the expected node" + ); + Assert.ok( + tree.controller.canMoveNode(tree.selectedNode), + "should be able to move user-created shortcuts to roots" + ); + } + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js b/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js new file mode 100644 index 0000000000..4e3d29cf21 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function onPopupEvent(popup, evt) { + let fullEvent = "popup" + evt; + return BrowserTestUtils.waitForEvent(popup, fullEvent, false, e => { + return e.target == popup; + }); +} + +add_task(async function test_bookmarks_menu() { + // On macOS, ctrl-click shouldn't open the panel because this normally opens + // the context menu. This happens via the `contextmenu` event which is created + // by widget code, so our simulated clicks do not do so, so we can't test + // anything on macOS. + if (AppConstants.platform == "macosx") { + ok(true, "The test is ignored on Mac"); + return; + } + + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR, + 4 + ); + + const button = document.getElementById("bookmarks-menu-button"); + const popup = document.getElementById("BMB_bookmarksPopup"); + + let shownPromise = onPopupEvent(popup, "shown"); + EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true }); + await shownPromise; + ok(true, "Bookmarks menu shown after button pressed"); + + // Close bookmarks popup. + let hiddenPromise = onPopupEvent(popup, "hidden"); + popup.hidePopup(); + await hiddenPromise; + + CustomizableUI.reset(); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_popup.js b/browser/components/places/tests/browser/browser_bookmark_popup.js new file mode 100644 index 0000000000..616755b7e1 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_popup.js @@ -0,0 +1,712 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; +requestLongerTimeout(2); + +/** + * Test opening and closing the bookmarks panel. + */ +let win; +let bookmarkPanel; +let bookmarkStar; +let bookmarkPanelTitle; +let bookmarkRemoveButton; +let editBookmarkPanelRemoveButtonRect; + +const TEST_URL = "data:text/html,<html><body></body></html>"; + +add_setup(async function () { + win = await BrowserTestUtils.openNewBrowserWindow(); + + win.StarUI._createPanelIfNeeded(); + win.StarUI._closePanelQuickForTesting = true; + bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + bookmarkStar = win.BookmarkingUI.star; + bookmarkPanelTitle = win.document.getElementById("editBookmarkPanelTitle"); + bookmarkRemoveButton = win.document.getElementById( + "editBookmarkPanelRemoveButton" + ); + + registerCleanupFunction(async () => { + delete win.StarUI._closePanelQuickForTesting; + await BrowserTestUtils.closeWindow(win); + }); +}); + +function mouseout() { + let mouseOutPromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "mouseout" + ); + EventUtils.synthesizeNativeMouseEvent({ + type: "mousemove", + target: win.gURLBar.textbox, + offsetX: 0, + offsetY: 0, + win, + }); + EventUtils.synthesizeMouse(bookmarkPanel, 0, 0, { type: "mouseout" }, win); + info("Waiting for mouseout event"); + return mouseOutPromise; +} + +async function test_bookmarks_popup({ + isNewBookmark, + popupShowFn, + popupEditFn, + shouldAutoClose, + popupHideFn, + isBookmarkRemoved, +}) { + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: TEST_URL }, + async function (browser) { + try { + if (!isNewBookmark) { + await PlacesUtils.bookmarks.insert({ + parentGuid: await PlacesUIUtils.defaultParentGuid, + url: TEST_URL, + title: "Home Page", + }); + } + + info(`BookmarkingUI.status is ${win.BookmarkingUI.status}`); + await TestUtils.waitForCondition( + () => win.BookmarkingUI.status != win.BookmarkingUI.STATUS_UPDATING, + "BookmarkingUI should not be updating" + ); + + Assert.equal( + bookmarkStar.hasAttribute("starred"), + !isNewBookmark, + "Page should only be starred prior to popupshown if editing bookmark" + ); + Assert.equal( + bookmarkPanel.state, + "closed", + "Panel should be 'closed' to start test" + ); + let shownPromise = promisePopupShown(bookmarkPanel); + await popupShowFn(browser); + await shownPromise; + Assert.equal( + bookmarkPanel.state, + "open", + "Panel should be 'open' after shownPromise is resolved" + ); + + editBookmarkPanelRemoveButtonRect = + bookmarkRemoveButton.getBoundingClientRect(); + + if (popupEditFn) { + await popupEditFn(); + } + Assert.equal( + bookmarkStar.getAttribute("starred"), + "true", + "Page is starred" + ); + Assert.equal( + bookmarkPanelTitle.dataset.l10nId, + isNewBookmark ? "bookmarks-add-bookmark" : "bookmarks-edit-bookmark", + "title should match isEditingBookmark state" + ); + Assert.equal( + bookmarkRemoveButton.dataset.l10nId, + isNewBookmark ? "bookmark-panel-cancel" : "bookmark-panel-remove", + "remove/cancel button label should match isEditingBookmark state" + ); + + if (!shouldAutoClose) { + await new Promise(resolve => setTimeout(resolve, 400)); + Assert.equal( + bookmarkPanel.state, + "open", + "Panel should still be 'open' for non-autoclose" + ); + } + + let defaultLocation = await PlacesUIUtils.defaultParentGuid; + const promises = []; + if (isNewBookmark && !isBookmarkRemoved) { + // Expect new bookmark to be created. + promises.push( + PlacesTestUtils.waitForNotification("bookmark-added", events => + events.some( + ({ parentGuid, url }) => + parentGuid == defaultLocation && TEST_URL == url + ) + ) + ); + } + if (!isNewBookmark && isBookmarkRemoved) { + // Expect existing bookmark to be removed. + promises.push( + PlacesTestUtils.waitForNotification("bookmark-removed", events => + events.some( + ({ parentGuid, url }) => + parentGuid == defaultLocation && TEST_URL == url + ) + ) + ); + } + + promises.push(promisePopupHidden(bookmarkPanel)); + if (popupHideFn) { + await popupHideFn(); + } else { + // Move the mouse out of the way so that the panel will auto-close. + await mouseout(); + } + await Promise.all(promises); + + Assert.equal( + bookmarkStar.hasAttribute("starred"), + !isBookmarkRemoved, + "Page is starred after closing" + ); + + // Count number of bookmarks. + let count = 0; + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, () => count++); + const message = isBookmarkRemoved + ? "No bookmark should exist" + : "Only one bookmark should exist"; + Assert.equal(count, isBookmarkRemoved ? 0 : 1, message); + } finally { + let bookmark = await PlacesUtils.bookmarks.fetch({ url: TEST_URL }); + Assert.equal( + !!bookmark, + !isBookmarkRemoved, + "bookmark should not be present if a panel action should've removed it" + ); + if (bookmark) { + await PlacesUtils.bookmarks.remove(bookmark); + } + } + } + ); +} + +add_task(async function panel_shown_for_new_bookmarks_and_autocloses() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + shouldAutoClose: true, + isBookmarkRemoved: false, + }); +}); + +add_task( + async function panel_shown_once_for_doubleclick_on_new_bookmark_star_and_autocloses() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + EventUtils.synthesizeMouse( + bookmarkStar, + 10, + 10, + { clickCount: 2 }, + win + ); + }, + shouldAutoClose: true, + isBookmarkRemoved: false, + }); + } +); + +add_task( + async function panel_shown_once_for_slow_doubleclick_on_new_bookmark_star_and_autocloses() { + todo( + false, + "bug 1250267, may need to add some tracking state to " + + "browser-places.js for this." + ); + + /* + await test_bookmarks_popup({ + isNewBookmark: true, + *popupShowFn() { + EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window); + await new Promise(resolve => setTimeout(resolve, 300)); + EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window); + }, + shouldAutoClose: true, + isBookmarkRemoved: false, + }); + */ + } +); + +add_task( + async function panel_shown_for_keyboardshortcut_on_new_bookmark_star_and_autocloses() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + EventUtils.synthesizeKey("D", { accelKey: true }, win); + }, + shouldAutoClose: true, + isBookmarkRemoved: false, + }); + } +); + +add_task(async function panel_shown_for_new_bookmarks_mousemove_mouseout() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + async popupEditFn() { + let mouseMovePromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "mousemove" + ); + EventUtils.synthesizeMouseAtCenter( + bookmarkPanel, + { type: "mousemove" }, + win + ); + info("Waiting for mousemove event"); + await mouseMovePromise; + info("Got mousemove event"); + + await new Promise(resolve => setTimeout(resolve, 400)); + is( + bookmarkPanel.state, + "open", + "Panel should still be open on mousemove" + ); + }, + async popupHideFn() { + await mouseout(); + info("Got mouseout event, should autoclose now"); + }, + shouldAutoClose: false, + isBookmarkRemoved: false, + }); +}); + +add_task(async function panel_shown_for_new_bookmark_close_with_ESC() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + shouldAutoClose: true, + popupHideFn() { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function panel_shown_for_editing_no_autoclose_close_with_ESC() { + await test_bookmarks_popup({ + isNewBookmark: false, + popupShowFn() { + bookmarkStar.click(); + }, + shouldAutoClose: false, + popupHideFn() { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + }, + isBookmarkRemoved: false, + }); +}); + +add_task(async function panel_shown_for_new_bookmark_keypress_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + popupEditFn() { + EventUtils.sendChar("VK_TAB", win); + }, + shouldAutoClose: false, + popupHideFn() { + bookmarkPanel.hidePopup(); + }, + isBookmarkRemoved: false, + }); +}); + +add_task(async function bookmark_with_invalid_default_folder() { + await createAndRemoveDefaultFolder(); + + await test_bookmarks_popup({ + isNewBookmark: true, + shouldAutoClose: true, + async popupShowFn(browser) { + EventUtils.synthesizeKey("d", { accelKey: true }, win); + }, + }); +}); + +add_task( + async function panel_shown_for_new_bookmark_compositionstart_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + async popupEditFn() { + let compositionStartPromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "compositionstart" + ); + EventUtils.synthesizeComposition({ type: "compositionstart" }, win); + info("Waiting for compositionstart event"); + await compositionStartPromise; + info("Got compositionstart event"); + }, + shouldAutoClose: false, + popupHideFn() { + EventUtils.synthesizeComposition( + { type: "compositioncommitasis" }, + win + ); + bookmarkPanel.hidePopup(); + }, + isBookmarkRemoved: false, + }); + } +); + +add_task( + async function panel_shown_for_new_bookmark_compositionstart_mouseout_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + async popupEditFn() { + let mouseMovePromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "mousemove" + ); + EventUtils.synthesizeMouseAtCenter( + bookmarkPanel, + { + type: "mousemove", + }, + win + ); + info("Waiting for mousemove event"); + await mouseMovePromise; + info("Got mousemove event"); + + let compositionStartPromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "compositionstart" + ); + EventUtils.synthesizeComposition({ type: "compositionstart" }, win); + info("Waiting for compositionstart event"); + await compositionStartPromise; + info("Got compositionstart event"); + + await mouseout(); + info("Got mouseout event, but shouldn't run autoclose"); + }, + shouldAutoClose: false, + popupHideFn() { + EventUtils.synthesizeComposition( + { type: "compositioncommitasis" }, + win + ); + bookmarkPanel.hidePopup(); + }, + isBookmarkRemoved: false, + }); + } +); + +add_task( + async function panel_shown_for_new_bookmark_compositionend_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn() { + bookmarkStar.click(); + }, + async popupEditFn() { + let mouseMovePromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "mousemove" + ); + EventUtils.synthesizeMouseAtCenter( + bookmarkPanel, + { + type: "mousemove", + }, + win + ); + info("Waiting for mousemove event"); + await mouseMovePromise; + info("Got mousemove event"); + + EventUtils.synthesizeComposition( + { + type: "compositioncommit", + data: "committed text", + }, + win + ); + }, + popupHideFn() { + bookmarkPanel.hidePopup(); + }, + shouldAutoClose: false, + isBookmarkRemoved: false, + }); + } +); + +add_task(async function contextmenu_new_bookmark_keypress_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + async popupShowFn(browser) { + let contextMenu = win.document.getElementById("contentAreaContextMenu"); + let awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + let awaitPopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "body", + { + type: "contextmenu", + button: 2, + }, + browser + ); + await awaitPopupShown; + contextMenu.activateItem( + win.document.getElementById("context-bookmarkpage") + ); + await awaitPopupHidden; + }, + popupEditFn() { + EventUtils.sendChar("VK_TAB", win); + }, + shouldAutoClose: false, + popupHideFn() { + bookmarkPanel.hidePopup(); + }, + isBookmarkRemoved: false, + }); +}); + +add_task(async function bookmarks_menu_new_bookmark_remove_bookmark() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn(browser) { + win.document.getElementById("menu_bookmarkThisPage").doCommand(); + }, + shouldAutoClose: true, + popupHideFn() { + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function ctrl_d_edit_bookmark_remove_bookmark() { + await test_bookmarks_popup({ + isNewBookmark: false, + popupShowFn(browser) { + EventUtils.synthesizeKey("D", { accelKey: true }, win); + }, + shouldAutoClose: true, + popupHideFn() { + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function enter_on_remove_bookmark_should_remove_bookmark() { + if (AppConstants.platform == "macosx") { + // "Full Keyboard Access" is disabled by default, and thus doesn't allow + // keyboard navigation to the "Remove Bookmarks" button by default. + return; + } + + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn(browser) { + EventUtils.synthesizeKey("D", { accelKey: true }, win); + }, + shouldAutoClose: true, + popupHideFn() { + while ( + !win.document.activeElement || + win.document.activeElement.id != "editBookmarkPanelRemoveButton" + ) { + EventUtils.sendChar("VK_TAB", win); + } + EventUtils.sendChar("VK_RETURN", win); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function mouse_hovering_panel_should_prevent_autoclose() { + if (AppConstants.platform != "win") { + // This test requires synthesizing native mouse movement which is + // best supported on Windows. + return; + } + await test_bookmarks_popup({ + isNewBookmark: true, + async popupShowFn() { + await EventUtils.promiseNativeMouseEvent({ + type: "mousemove", + target: win.document.documentElement, + offsetX: editBookmarkPanelRemoveButtonRect.left, + offsetY: editBookmarkPanelRemoveButtonRect.top, + }); + EventUtils.synthesizeKey("D", { accelKey: true }, win); + }, + shouldAutoClose: false, + popupHideFn() { + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function ctrl_d_new_bookmark_mousedown_mouseout_no_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: true, + popupShowFn(browser) { + EventUtils.synthesizeKey("D", { accelKey: true }, win); + }, + async popupEditFn() { + let mouseMovePromise = BrowserTestUtils.waitForEvent( + bookmarkPanel, + "mousemove" + ); + EventUtils.synthesizeMouseAtCenter( + bookmarkPanel, + { type: "mousemove" }, + win + ); + info("Waiting for mousemove event"); + await mouseMovePromise; + info("Got mousemove event"); + + await new Promise(resolve => setTimeout(resolve, 400)); + is( + bookmarkPanel.state, + "open", + "Panel should still be open on mousemove" + ); + + EventUtils.synthesizeMouseAtCenter( + bookmarkPanelTitle, + { + button: 1, + type: "mousedown", + }, + win + ); + + await mouseout(); + }, + shouldAutoClose: false, + popupHideFn() { + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + }, + isBookmarkRemoved: true, + }); +}); + +add_task(async function enter_during_autocomplete_should_prevent_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: false, + async popupShowFn(browser) { + PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["Abc"]); + EventUtils.synthesizeKey("d", { accelKey: true }, win); + }, + async popupEditFn() { + // Start autocomplete with the registered tag. + let tagsField = win.document.getElementById("editBMPanel_tagsField"); + tagsField.value = ""; + let popup = win.document.getElementById("editBMPanel_tagsAutocomplete"); + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + tagsField.focus(); + EventUtils.sendString("a", win); + await promiseShown; + ok(promiseShown, "autocomplete shown"); + + // Select first candidate. + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + + // Type Enter key to choose the item. + EventUtils.synthesizeKey("KEY_Enter", {}, win); + + Assert.equal( + tagsField.value, + "Abc", + "Autocomplete should've inserted the selected item" + ); + }, + shouldAutoClose: false, + popupHideFn() { + EventUtils.synthesizeKey("KEY_Escape", {}, win); + }, + isBookmarkRemoved: false, + }); +}); + +add_task(async function escape_during_autocomplete_should_prevent_autoclose() { + await test_bookmarks_popup({ + isNewBookmark: false, + async popupShowFn(browser) { + PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["Abc"]); + EventUtils.synthesizeKey("d", { accelKey: true }, win); + }, + async popupEditFn() { + // Start autocomplete with the registered tag. + let tagsField = win.document.getElementById("editBMPanel_tagsField"); + tagsField.value = ""; + let popup = win.document.getElementById("editBMPanel_tagsAutocomplete"); + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + tagsField.focus(); + EventUtils.sendString("a", win); + await promiseShown; + ok(promiseShown, "autocomplete shown"); + + // Select first candidate. + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + + // Type Escape key to close autocomplete. + EventUtils.synthesizeKey("KEY_Escape", {}, win); + + // The text reverts to what was typed. + // Note, it's important that this is different from the previously + // inserted tag, since it will test an untag/tag undo condition. + Assert.equal( + tagsField.value, + "a", + "Autocomplete should revert to what was typed" + ); + }, + shouldAutoClose: false, + popupHideFn() { + EventUtils.synthesizeKey("KEY_Escape", {}, win); + }, + isBookmarkRemoved: false, + }); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_private_window.js b/browser/components/places/tests/browser/browser_bookmark_private_window.js new file mode 100644 index 0000000000..2557833816 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_private_window.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the a website can be bookmarked from a private window. + */ +"use strict"; + +const TEST_URL = "about:buildconfig"; + +// Cleanup. +registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_add_bookmark_from_private_window() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + registerCleanupFunction(async () => { + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + }); + + // Open the bookmark panel. + win.StarUI._createPanelIfNeeded(); + let bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + let shownPromise = promisePopupShown(bookmarkPanel); + let bookmarkAddedPromise = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => url === TEST_URL) + ); + let bookmarkStar = win.BookmarkingUI.star; + bookmarkStar.click(); + await shownPromise; + + // Check if the bookmark star changes its state after click. + Assert.equal( + bookmarkStar.getAttribute("starred"), + "true", + "Bookmark star changed its state correctly." + ); + + // Close the bookmark panel. + let hiddenPromise = promisePopupHidden(bookmarkPanel); + let doneButton = win.document.getElementById("editBookmarkPanelDoneButton"); + doneButton.click(); + await hiddenPromise; + await bookmarkAddedPromise; + + let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL }); + Assert.equal( + bm.url, + TEST_URL, + "The bookmark was successfully saved in the database." + ); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_remove_tags.js b/browser/components/places/tests/browser/browser_bookmark_remove_tags.js new file mode 100644 index 0000000000..ece5fdbce0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_remove_tags.js @@ -0,0 +1,256 @@ +/** + * Tests that the bookmark tags can be removed from the bookmark star, toolbar and sidebar. + */ +"use strict"; + +const TEST_URL = "about:buildconfig"; +const TEST_URI = Services.io.newURI(TEST_URL); +const TEST_TAG = "tag"; + +// Setup. +add_setup(async function () { + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + // Cleanup. + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_remove_tags_from_BookmarkStar() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: TEST_URL, + }); + PlacesUtils.tagging.tagURI(TEST_URI, ["tag1", "tag2", "tag3", "tag4"]); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + BrowserTestUtils.removeTab(tab); + }); + + StarUI._createPanelIfNeeded(); + await clickBookmarkStar(); + + // Check if the "Edit This Bookmark" panel is open. + let bookmarkPanelTitle = document.getElementById("editBookmarkPanelTitle"); + Assert.equal( + document.l10n.getAttributes(bookmarkPanelTitle).id, + "bookmarks-edit-bookmark", + "Bookmark panel title is correct." + ); + + let promiseTagsChange = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + + // Update the "tags" field. + fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2, tag3", window); + let tagspicker = document.getElementById("editBMPanel_tagsField"); + await TestUtils.waitForCondition( + () => tagspicker.value === "tag1, tag2, tag3", + "Tags are correct after update." + ); + + let doneButton = document.getElementById("editBookmarkPanelDoneButton"); + doneButton.click(); + await promiseTagsChange; + + let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI); + Assert.deepEqual( + tags, + ["tag1", "tag2", "tag3"], + "Should have updated the bookmark tags in the database." + ); +}); + +add_task(async function test_remove_tags_from_Toolbar() { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: TEST_URL, + url: TEST_URL, + }); + + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + await withBookmarksDialog( + false, + async function openPropertiesDialog() { + let placesContext = document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await promisePopup; + + let properties = document.getElementById( + "placesContext_show_bookmark:info" + ); + placesContext.activateItem(properties, {}); + }, + async function test(dialogWin) { + let tagspicker = dialogWin.document.getElementById( + "editBMPanel_tagsField" + ); + Assert.equal( + tagspicker.value, + "tag1, tag2, tag3", + "Tags are correct before update." + ); + + let promiseTagsChange = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + + // Update the "tags" field. + fillBookmarkTextField( + "editBMPanel_tagsField", + "tag1, tag2", + dialogWin, + false + ); + await TestUtils.waitForCondition( + () => tagspicker.value === "tag1, tag2", + "Tags are correct after update." + ); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + await promiseTagsChange; + + let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI); + Assert.deepEqual( + tags, + ["tag1", "tag2"], + "Should have updated the bookmark tags in the database." + ); + } + ); +}); + +add_task(async function test_remove_tags_from_Sidebar() { + let bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => + bookmarks.push(bm) + ); + + await withSidebarTree("bookmarks", async function (tree) { + tree.selectItems([bookmarks[0].guid]); + + await withBookmarksDialog( + false, + function openPropertiesDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + let tagspicker = dialogWin.document.getElementById( + "editBMPanel_tagsField" + ); + Assert.equal( + tagspicker.value, + "tag1, tag2", + "Tags are correct before update." + ); + + let promiseTagsChange = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + + // Update the "tags" field. + fillBookmarkTextField( + "editBMPanel_tagsField", + "tag1", + dialogWin, + false + ); + await TestUtils.waitForCondition( + () => tagspicker.value === "tag1", + "Tags are correct after update." + ); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + await promiseTagsChange; + + let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI); + Assert.deepEqual( + tags, + ["tag1"], + "Should have updated the bookmark tags in the database." + ); + } + ); + }); +}); + +add_task(async function test_remove_tags_from_Library() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: TEST_URL, + }); + PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]); + const getTags = () => PlacesUtils.tagging.getTagsForURI(TEST_URI); + + // Open the Library and select the tag. + const library = await promiseLibrary("place:tag=" + TEST_TAG); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + + const contextMenu = library.document.getElementById("placesContext"); + const contextMenuDeleteTag = library.document.getElementById( + "placesContext_removeTag" + ); + + let firstColumn = library.ContentTree.view.columns[0]; + let firstBookmarkRect = library.ContentTree.view.getCoordsForCellItem( + 0, + firstColumn, + "bm0" + ); + + EventUtils.synthesizeMouse( + library.ContentTree.view.body, + firstBookmarkRect.x, + firstBookmarkRect.y, + { type: "contextmenu", button: 2 }, + library + ); + + await BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + + ok(getTags().includes(TEST_TAG), "Test tag exists before delete."); + + contextMenu.activateItem(contextMenuDeleteTag, {}); + + await PlacesTestUtils.waitForNotification("bookmark-tags-changed"); + await promiseLibraryClosed(library); + + ok( + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }), + "Bookmark still exists after removing tag." + ); + ok(!getTags().includes(TEST_TAG), "Test tag is removed after delete."); +}); diff --git a/browser/components/places/tests/browser/browser_bookmark_titles.js b/browser/components/places/tests/browser/browser_bookmark_titles.js new file mode 100644 index 0000000000..c91f9559e7 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_titles.js @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is tests for the default titles that new bookmarks get. + +var tests = [ + // Common page. + { + url: "http://example.com/browser/browser/components/places/tests/browser/dummy_page.html", + title: "Dummy test page", + isError: false, + }, + // Data URI. + { + url: "data:text/html;charset=utf-8,<title>test%20data:%20url</title>", + title: "test data: url", + isError: false, + }, + // about:neterror + { + url: "data:application/xhtml+xml,", + title: "data:application/xhtml+xml,", + isError: true, + }, + // about:certerror + { + url: "https://untrusted.example.com/somepage.html", + title: "https://untrusted.example.com/somepage.html", + isError: true, + }, +]; + +SpecialPowers.pushPrefEnv({ + set: [["browser.bookmarks.editDialog.showForNewBookmarks", false]], +}); + +add_task(async function check_default_bookmark_title() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://www.example.com/" + ); + let browser = tab.linkedBrowser; + + // Test that a bookmark of each URI gets the corresponding default title. + for (let { url, title, isError } of tests) { + let promiseLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + url, + isError + ); + BrowserTestUtils.startLoadingURIString(browser, url); + await promiseLoaded; + + await checkBookmark(url, title); + } + + // Network failure test: now that dummy_page.html is in history, bookmarking + // it should give the last known page title as the default bookmark title. + + // Simulate a network outage with offline mode. (Localhost is still + // accessible in offline mode, so disable the test proxy as well.) + BrowserOffline.toggleOfflineStatus(); + let proxy = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + registerCleanupFunction(function () { + BrowserOffline.toggleOfflineStatus(); + Services.prefs.setIntPref("network.proxy.type", proxy); + }); + + // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache. + Services.cache2.clear(); + + let { url, title } = tests[0]; + + let promiseLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + null, + true + ); + BrowserTestUtils.startLoadingURIString(browser, url); + await promiseLoaded; + + // The offline mode test is only good if the page failed to load. + await SpecialPowers.spawn(browser, [], function () { + Assert.equal( + content.document.documentURI.substring(0, 14), + "about:neterror", + "Offline mode successfully simulated network outage." + ); + }); + await checkBookmark(url, title); + + BrowserTestUtils.removeTab(tab); +}); + +// Bookmark the current page and confirm that the new bookmark has the expected +// title. (Then delete the bookmark.) +async function checkBookmark(url, expected_title) { + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + url, + "Trying to bookmark the expected uri" + ); + + let promiseBookmark = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => + events.some( + ({ url: eventUrl }) => + eventUrl == gBrowser.selectedBrowser.currentURI.spec + ) + ); + PlacesCommandHook.bookmarkPage(); + await promiseBookmark; + + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + + Assert.ok(bookmark, "Found the expected bookmark"); + Assert.equal( + bookmark.title, + expected_title, + "Bookmark got a good default title." + ); + + await PlacesUtils.bookmarks.remove(bookmark); +} diff --git a/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js new file mode 100644 index 0000000000..69f6298b8c --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js @@ -0,0 +1,79 @@ +"use strict"; + +let BASE_URL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const TEST_URL = BASE_URL + "pageopeningwindow.html"; +const DUMMY_URL = BASE_URL + "bookmarklet_windowOpen_dummy.html"; + +function makeBookmarkFor(url, keyword) { + return Promise.all([ + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmarklet", + url, + }), + PlacesUtils.keywords.insert({ url, keyword }), + ]); +} + +add_task(async function openKeywordBookmarkWithWindowOpen() { + // This is the current default, but let's not assume that... + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.link.open_newwindow", 3], + ["dom.disable_open_during_load", true], + ], + }); + + let moztab; + let tabOpened = BrowserTestUtils.openNewForegroundTab( + gBrowser, + DUMMY_URL + ).then(tab => { + moztab = tab; + }); + let keywordForBM = "openmeatab"; + + let bookmarkInfo; + let bookmarkCreated = makeBookmarkFor( + "javascript:void open('" + TEST_URL + "')", + keywordForBM + ).then(values => { + bookmarkInfo = values[0]; + }); + await Promise.all([tabOpened, bookmarkCreated]); + + registerCleanupFunction(function () { + return Promise.all([ + PlacesUtils.bookmarks.remove(bookmarkInfo), + PlacesUtils.keywords.remove(keywordForBM), + ]); + }); + gURLBar.value = keywordForBM; + gURLBar.focus(); + + let tabCreatedPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter"); + + info("Waiting for tab being created"); + let { target: tab } = await tabCreatedPromise; + info("Got tab"); + let browser = tab.linkedBrowser; + if (!browser.currentURI || browser.currentURI.spec != TEST_URL) { + info("Waiting for browser load"); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL); + } + is( + browser.currentURI && browser.currentURI.spec, + TEST_URL, + "Tab with expected URL loaded." + ); + info("Waiting to remove tab"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(moztab); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarksProperties.js b/browser/components/places/tests/browser/browser_bookmarksProperties.js new file mode 100644 index 0000000000..8f1d783a49 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js @@ -0,0 +1,532 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the bookmarks Properties dialog. + */ + +// DOM ids of Places sidebar trees. +const SIDEBAR_HISTORY_TREE_ID = "historyTree"; +const SIDEBAR_BOOKMARKS_TREE_ID = "bookmarks-view"; + +const SIDEBAR_HISTORY_ID = "viewHistorySidebar"; +const SIDEBAR_BOOKMARKS_ID = "viewBookmarksSidebar"; + +// For history sidebar. +const SIDEBAR_HISTORY_BYLASTVISITED_VIEW = "bylastvisited"; +const SIDEBAR_HISTORY_BYMOSTVISITED_VIEW = "byvisited"; +const SIDEBAR_HISTORY_BYDATE_VIEW = "byday"; +const SIDEBAR_HISTORY_BYSITE_VIEW = "bysite"; +const SIDEBAR_HISTORY_BYDATEANDSITE_VIEW = "bydateandsite"; + +// Action to execute on the current node. +const ACTION_EDIT = 0; +const ACTION_ADD = 1; + +// If action is ACTION_ADD, set type to one of those, to define what do you +// want to create. +const TYPE_FOLDER = 0; +const TYPE_BOOKMARK = 1; + +const TEST_URL = "http://www.example.com/"; + +const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xhtml"; + +function add_bookmark(url) { + return PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: `bookmark/${url}`, + }); +} + +// Each test is an obj w/ a desc property and run method. +var gTests = []; + +// ------------------------------------------------------------------------------ +// Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog + +gTests.push({ + desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog", + sidebar: SIDEBAR_BOOKMARKS_ID, + action: ACTION_EDIT, + itemType: null, + window: null, + _bookmark: null, + _cleanShutdown: false, + + async setup() { + // Add a bookmark in unsorted bookmarks folder. + this._bookmark = await add_bookmark(TEST_URL); + Assert.ok(this._bookmark, "Correctly added a bookmark"); + + // Add a tag to this bookmark. + PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]); + var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)); + Assert.equal(tags[0], "testTag", "Correctly added a tag"); + }, + + selectNode(tree) { + tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + tree.selectItems([this._bookmark.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + this._bookmark.guid, + "Bookmark has been selected" + ); + }, + + async run() { + // open tags autocomplete and press enter + var tagsField = this.window.document.getElementById( + "editBMPanel_tagsField" + ); + var self = this; + + let unloadPromise = new Promise(resolve => { + this.window.addEventListener( + "unload", + function (event) { + tagsField.popup.removeEventListener( + "popuphidden", + popupListener, + true + ); + Assert.ok( + self._cleanShutdown, + "Dialog window should not be closed by pressing Enter on the autocomplete popup" + ); + executeSoon(function () { + resolve(); + }); + }, + { capture: true, once: true } + ); + }); + + var popupListener = { + handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphidden": + // Everything worked fine, we can stop observing the window. + self._cleanShutdown = true; + self.window.document + .getElementById("bookmarkpropertiesdialog") + .cancelDialog(); + break; + case "popupshown": + tagsField.popup.removeEventListener("popupshown", this, true); + // In case this test fails the window will close, the test will fail + // since we didn't set _cleanShutdown. + let richlistbox = tagsField.popup.richlistbox; + // Focus and select first result. + Assert.equal( + richlistbox.itemCount, + 1, + "We have 1 autocomplete result" + ); + tagsField.popup.selectedIndex = 0; + Assert.equal( + richlistbox.selectedItems.length, + 1, + "We have selected a tag from the autocomplete popup" + ); + info("About to focus the autocomplete results"); + richlistbox.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}, self.window); + break; + default: + Assert.ok(false, "unknown event: " + aEvent.type); + } + }, + }; + tagsField.popup.addEventListener("popupshown", popupListener, true); + tagsField.popup.addEventListener("popuphidden", popupListener, true); + + // Open tags autocomplete popup. + info("About to focus the tagsField"); + executeSoon(() => { + tagsField.focus(); + tagsField.value = ""; + EventUtils.synthesizeKey("t", {}, this.window); + }); + await unloadPromise; + }, + + finish() { + SidebarUI.hide(); + }, + + async cleanup() { + // Check tags have not changed. + var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)); + Assert.equal(tags[0], "testTag", "Tag on node has not changed"); + + // Cleanup. + PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]); + await PlacesUtils.bookmarks.remove(this._bookmark); + let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid); + Assert.ok(!bm, "should have been removed"); + }, +}); + +// ------------------------------------------------------------------------------ +// Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel + +gTests.push({ + desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel", + sidebar: SIDEBAR_BOOKMARKS_ID, + action: ACTION_EDIT, + itemType: null, + window: null, + _bookmark: null, + _cleanShutdown: false, + + async setup() { + // Add a bookmark in unsorted bookmarks folder. + this._bookmark = await add_bookmark(TEST_URL); + Assert.ok(this._bookmark, "Correctly added a bookmark"); + + // Add a tag to this bookmark. + PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]); + var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)); + Assert.equal(tags[0], "testTag", "Correctly added a tag"); + }, + + selectNode(tree) { + tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + tree.selectItems([this._bookmark.guid]); + Assert.equal( + tree.selectedNode.bookmarkGuid, + this._bookmark.guid, + "Bookmark has been selected" + ); + }, + + async run() { + // open tags autocomplete and press enter + var tagsField = this.window.document.getElementById( + "editBMPanel_tagsField" + ); + var self = this; + + let hiddenPromise = new Promise(resolve => { + this.window.addEventListener( + "unload", + function (event) { + tagsField.popup.removeEventListener( + "popuphidden", + popupListener, + true + ); + Assert.ok( + self._cleanShutdown, + "Dialog window should not be closed by pressing Escape on the autocomplete popup" + ); + executeSoon(function () { + resolve(); + }); + }, + { capture: true, once: true } + ); + }); + + var popupListener = { + handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphidden": + // Everything worked fine. + self._cleanShutdown = true; + self.window.document + .getElementById("bookmarkpropertiesdialog") + .cancelDialog(); + break; + case "popupshown": + tagsField.popup.removeEventListener("popupshown", this, true); + // In case this test fails the window will close, the test will fail + // since we didn't set _cleanShutdown. + let richlistbox = tagsField.popup.richlistbox; + // Focus and select first result. + Assert.equal( + richlistbox.itemCount, + 1, + "We have 1 autocomplete result" + ); + tagsField.popup.selectedIndex = 0; + Assert.equal( + richlistbox.selectedItems.length, + 1, + "We have selected a tag from the autocomplete popup" + ); + info("About to focus the autocomplete results"); + richlistbox.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window); + break; + default: + Assert.ok(false, "unknown event: " + aEvent.type); + } + }, + }; + tagsField.popup.addEventListener("popupshown", popupListener, true); + tagsField.popup.addEventListener("popuphidden", popupListener, true); + + // Open tags autocomplete popup. + info("About to focus the tagsField"); + tagsField.focus(); + tagsField.value = ""; + EventUtils.synthesizeKey("t", {}, this.window); + await hiddenPromise; + }, + + finish() { + SidebarUI.hide(); + }, + + async cleanup() { + // Check tags have not changed. + var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)); + Assert.equal(tags[0], "testTag", "Tag on node has not changed"); + + // Cleanup. + PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]); + await PlacesUtils.bookmarks.remove(this._bookmark); + let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid); + Assert.ok(!bm, "should have been removed"); + }, +}); + +// ------------------------------------------------------------------------------ +// Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog + +gTests.push({ + desc: `Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog`, + sidebar: SIDEBAR_HISTORY_ID, + dialogUrl: DIALOG_URL, + action: ACTION_ADD, + historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW, + window: null, + + async setup() { + // Add a visit. + await PlacesTestUtils.addVisits(TEST_URL); + }, + + selectNode(tree) { + var visitNode = tree.view.nodeForTreeIndex(0); + tree.selectNode(visitNode); + Assert.equal( + tree.selectedNode.uri, + TEST_URL, + "The correct visit has been selected" + ); + Assert.equal( + tree.selectedNode.itemId, + -1, + "The selected node is not bookmarked" + ); + }, + + async run() { + // Open folder selector. + var foldersExpander = this.window.document.getElementById( + "editBMPanel_foldersExpander" + ); + var folderTree = this.window.gEditItemOverlay._folderTree; + var self = this; + + let unloadPromise = new Promise(resolve => { + this.window.addEventListener( + "unload", + event => { + Assert.ok( + self._cleanShutdown, + "Dialog window should not be closed by pressing ESC in folder name textbox" + ); + executeSoon(() => { + resolve(); + }); + }, + { capture: true, once: true } + ); + }); + + const observer = new this.window.MutationObserver( + (aMutationList, aObserver) => { + for (const mutation of aMutationList) { + if ( + mutation.type != "attributes" || + mutation.attributeName != "place" + ) { + continue; + } + aObserver.disconnect(); + executeSoon(async function () { + // Create a new folder. + var newFolderButton = self.window.document.getElementById( + "editBMPanel_newFolderButton" + ); + newFolderButton.doCommand(); + + // Wait for the folder to be created and for editing to start. + await TestUtils.waitForCondition( + () => folderTree.hasAttribute("editing"), + "We are editing new folder name in folder tree" + ); + + // Press Escape to discard editing new folder name. + EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window); + Assert.ok( + !folderTree.hasAttribute("editing"), + "We have finished editing folder name in folder tree" + ); + + self._cleanShutdown = true; + + self.window.document + .getElementById("bookmarkpropertiesdialog") + .cancelDialog(); + }); + break; + } + } + ); + observer.observe(folderTree, { attributes: true }); + foldersExpander.doCommand(); + await unloadPromise; + }, + + finish() { + SidebarUI.hide(); + }, + + async cleanup() { + await PlacesTestUtils.promiseAsyncUpdates(); + + await PlacesUtils.history.clear(); + }, +}); + +// ------------------------------------------------------------------------------ + +add_task(async function test_setup() { + // This test can take some time, if we timeout too early it could run + // in the middle of other tests, or hang them. + requestLongerTimeout(2); +}); + +add_task(async function test_run() { + for (let test of gTests) { + info(`Start of test: ${test.desc}`); + await test.setup(); + + await execute_test_in_sidebar(test); + await test.run(); + + await test.cleanup(); + await test.finish(); + + info(`End of test: ${test.desc}`); + } +}); + +/** + * Global functions to run a test in Properties dialog context. + */ + +function execute_test_in_sidebar(test) { + return new Promise(resolve => { + var sidebar = document.getElementById("sidebar"); + sidebar.addEventListener( + "load", + function () { + // Need to executeSoon since the tree is initialized on sidebar load. + executeSoon(async () => { + await open_properties_dialog(test); + resolve(); + }); + }, + { capture: true, once: true } + ); + SidebarUI.show(test.sidebar); + }); +} + +async function promise_properties_window(dialogUrl = DIALOG_URL) { + let win = await BrowserTestUtils.promiseAlertDialogOpen(null, dialogUrl, { + isSubDialog: true, + }); + await SimpleTest.promiseFocus(win); + await win.document.mozSubdialogReady; + return win; +} + +async function open_properties_dialog(test) { + var sidebar = document.getElementById("sidebar"); + + // If this is history sidebar, set the required view. + if (test.sidebar == SIDEBAR_HISTORY_ID) { + sidebar.contentDocument.getElementById(test.historyView).doCommand(); + } + + // Get sidebar's Places tree. + var sidebarTreeID = + test.sidebar == SIDEBAR_BOOKMARKS_ID + ? SIDEBAR_BOOKMARKS_TREE_ID + : SIDEBAR_HISTORY_TREE_ID; + var tree = sidebar.contentDocument.getElementById(sidebarTreeID); + // The sidebar may take a moment to open from the doCommand, therefore wait + // until it has opened before continuing. + await TestUtils.waitForCondition(() => tree, "Sidebar tree has been loaded"); + + // Ask current test to select the node to edit. + test.selectNode(tree); + Assert.ok( + tree.selectedNode, + "We have a places node selected: " + tree.selectedNode.title + ); + + return new Promise(resolve => { + var command = null; + switch (test.action) { + case ACTION_EDIT: + command = "placesCmd_show:info"; + break; + case ACTION_ADD: + if (test.sidebar == SIDEBAR_BOOKMARKS_ID) { + if (test.itemType == TYPE_FOLDER) { + command = "placesCmd_new:folder"; + } else if (test.itemType == TYPE_BOOKMARK) { + command = "placesCmd_new:bookmark"; + } else { + Assert.ok( + false, + "You didn't set a valid itemType for adding an item" + ); + } + } else { + command = "placesCmd_createBookmark"; + } + break; + default: + Assert.ok(false, "You didn't set a valid action for this test"); + } + // Ensure command is enabled for this node. + Assert.ok( + tree.controller.isCommandEnabled(command), + " command '" + command + "' on current selected node is enabled" + ); + + promise_properties_window(test.dialogUrl).then(win => { + test.window = win; + resolve(); + }); + // This will open the dialog. For some reason this needs to be executed + // later, as otherwise opening the dialog throws an exception. + executeSoon(() => { + tree.controller.doCommand(command); + }); + }); +} diff --git a/browser/components/places/tests/browser/browser_bookmarks_change_title.js b/browser/components/places/tests/browser/browser_bookmarks_change_title.js new file mode 100644 index 0000000000..0e24df188a --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarks_change_title.js @@ -0,0 +1,256 @@ +/** + * Tests that the title of a bookmark can be changed from the bookmark star, toolbar, sidebar, and library. + */ +"use strict"; + +const TEST_URL = "about:buildconfig"; +const titleAfterFirstUpdate = "BookmarkStar title"; + +function getToolbarNodeForItemGuid(aItemGuid) { + var children = document.getElementById("PlacesToolbarItems").children; + for (let child of children) { + if (aItemGuid == child._placesNode.bookmarkGuid) { + return child; + } + } + return null; +} + +// Setup. +add_setup(async function () { + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_change_title_from_BookmarkStar() { + let originalBm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "Before Edit", + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + }); + + win.StarUI._createPanelIfNeeded(); + let bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + let shownPromise = promisePopupShown(bookmarkPanel); + + let bookmarkStar = win.BookmarkingUI.star; + bookmarkStar.click(); + + await shownPromise; + + let bookmarkPanelTitle = win.document.getElementById( + "editBookmarkPanelTitle" + ); + await BrowserTestUtils.waitForCondition( + () => + bookmarkPanelTitle.textContent === + gFluentStrings.formatValueSync("bookmarks-edit-bookmark"), + "Wait until the bookmark panel title will be changed expectedly." + ); + Assert.ok(true, "Bookmark title is correct"); + + let promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-title-changed", + events => events.some(e => e.title === titleAfterFirstUpdate) + ); + + // Update the bookmark's title. + await fillBookmarkTextField( + "editBMPanel_namePicker", + titleAfterFirstUpdate, + win + ); + + let doneButton = win.document.getElementById("editBookmarkPanelDoneButton"); + doneButton.click(); + await promiseNotification; + + let updatedBm = await PlacesUtils.bookmarks.fetch(originalBm.guid); + Assert.equal( + updatedBm.title, + titleAfterFirstUpdate, + "Should have updated the bookmark title in the database" + ); + await PlacesUtils.bookmarks.remove(originalBm.guid); +}); + +add_task(async function test_change_title_from_Toolbar() { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: titleAfterFirstUpdate, + url: TEST_URL, + }); + + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + await withBookmarksDialog( + false, + async function openPropertiesDialog() { + let placesContext = document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await promisePopup; + + let properties = document.getElementById( + "placesContext_show_bookmark:info" + ); + placesContext.activateItem(properties, {}); + }, + async function test(dialogWin) { + // Ensure the dialog has initialized. + await TestUtils.waitForCondition(() => dialogWin.document.title); + + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + + let editBookmarkDialogTitle = + dialogWin.document.getElementById("titleText"); + let bundle = dialogWin.document.getElementById("stringBundle"); + + Assert.equal( + bundle.getString("dialogTitleEditBookmark2"), + editBookmarkDialogTitle.textContent + ); + + Assert.equal( + namepicker.value, + titleAfterFirstUpdate, + "Name field is correct before update." + ); + + let promiseTitleChange = PlacesTestUtils.waitForNotification( + "bookmark-title-changed", + events => events.some(e => e.title === "Toolbar title") + ); + + // Update the bookmark's title. + fillBookmarkTextField( + "editBMPanel_namePicker", + "Toolbar title", + dialogWin, + false + ); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + await promiseTitleChange; + + let updatedBm = await PlacesUtils.bookmarks.fetch(toolbarBookmark.guid); + Assert.equal( + updatedBm.title, + "Toolbar title", + "Should have updated the bookmark title in the database" + ); + } + ); +}); + +add_task(async function test_change_title_from_Sidebar() { + let bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => + bookmarks.push(bm) + ); + + await withSidebarTree("bookmarks", async function (tree) { + tree.selectItems([bookmarks[0].guid]); + + await withBookmarksDialog( + false, + function openPropertiesDialog() { + tree.controller.doCommand("placesCmd_show:info"); + }, + async function test(dialogWin) { + let namepicker = dialogWin.document.getElementById( + "editBMPanel_namePicker" + ); + Assert.equal( + namepicker.value, + "Toolbar title", + "Name field is correct before update." + ); + + let promiseTitleChange = PlacesTestUtils.waitForNotification( + "bookmark-title-changed", + events => events.some(e => e.title === "Sidebar Title") + ); + + // Update the bookmark's title. + fillBookmarkTextField( + "editBMPanel_namePicker", + "Sidebar Title", + dialogWin, + false + ); + + // Confirm and close the dialog. + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + await promiseTitleChange; + + // Get updated bookmarks, check the new title. + bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => + bookmarks.push(bm) + ); + Assert.equal( + bookmarks[0].title, + "Sidebar Title", + "Should have updated the bookmark title in the database" + ); + Assert.equal(bookmarks.length, 1, "One bookmark should exist"); + } + ); + }); +}); + +add_task(async function test_change_title_from_Library() { + info("Open library and select the bookmark."); + const library = await promiseLibrary("BookmarksToolbar"); + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + library.ContentTree.view.selectNode( + library.ContentTree.view.view.nodeForTreeIndex(0) + ); + const newTitle = "Library"; + const promiseTitleChange = PlacesTestUtils.waitForNotification( + "bookmark-title-changed", + events => events.some(e => e.title === newTitle) + ); + info("Update the bookmark's title."); + fillBookmarkTextField("editBMPanel_namePicker", newTitle, library); + await promiseTitleChange; + info("The bookmark's title was updated."); + await promiseLibraryClosed(library); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarks_change_url.js b/browser/components/places/tests/browser/browser_bookmarks_change_url.js new file mode 100644 index 0000000000..ab5ad742d4 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarks_change_url.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* + * Test whether or not that url field in library window will update properly + * when changing it. + */ + +const TEST_URLS = ["https://example.com/", "https://example.org/"]; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function basic() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[0], + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[1], + }); + + info("Open library for bookmarks toolbar"); + const library = await promiseLibrary("BookmarksToolbar"); + + info("Check the initial content"); + const tree = library.document.getElementById("placeContent"); + Assert.equal(tree.view.rowCount, 2); + assertRow(tree, 0, TEST_URLS[0]); + assertRow(tree, 1, TEST_URLS[1]); + + info("Check the url"); + const newURL = `${TEST_URLS[0]}/?test`; + await updateURL(newURL, library); + + info("Check the update"); + Assert.equal(tree.view.rowCount, 2); + assertRow(tree, 0, newURL); + assertRow(tree, 1, TEST_URLS[1]); + + info("Close library window"); + await promiseLibraryClosed(library); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function whileFiltering() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[0], + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[1], + }); + + info("Open library for bookmarks toolbar"); + const library = await promiseLibrary("BookmarksToolbar"); + + info("Check the initial content"); + const tree = library.document.getElementById("placeContent"); + Assert.equal(tree.view.rowCount, 2); + assertRow(tree, 0, TEST_URLS[0]); + assertRow(tree, 1, TEST_URLS[1]); + + info("Filter by search chars"); + library.PlacesSearchBox.search("org"); + Assert.equal(tree.view.rowCount, 1); + assertRow(tree, 0, TEST_URLS[1]); + + info("Check the url"); + const newURL = `${TEST_URLS[1]}/?test`; + await updateURL(newURL, library); + + info("Check the update"); + Assert.equal(tree.view.rowCount, 1); + assertRow(tree, 0, newURL); + + info("Close library window"); + await promiseLibraryClosed(library); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +async function updateURL(newURL, library) { + const promiseUrlChange = PlacesTestUtils.waitForNotification( + "bookmark-url-changed", + () => true + ); + fillBookmarkTextField("editBMPanel_locationField", newURL, library); + await promiseUrlChange; +} + +function assertRow(tree, targeRow, expectedUrl) { + const url = tree.view.getCellText(targeRow, tree.columns.placesContentUrl); + Assert.equal(url, expectedUrl, "URL is correct"); +} diff --git a/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js b/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js new file mode 100644 index 0000000000..a425a9b031 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js @@ -0,0 +1,213 @@ +/** + * Test searching for bookmarks (by title and by tag) from the Bookmarks sidebar. + */ +"use strict"; + +let sidebar = document.getElementById("sidebar"); + +const TEST_URI = "http://example.com/"; +const BOOKMARKS_COUNT = 4; +const TEST_PARENT_FOLDER = "testParentFolder"; +const TEST_SIF_URL = "http://testsif.example.com/"; +const TEST_SIF_TITLE = "TestSIF"; +const TEST_NEW_TITLE = "NewTestSIF"; + +function assertBookmarks(searchValue) { + let found = 0; + + let searchBox = sidebar.contentDocument.getElementById("search-box"); + + ok(searchBox, "search box is in context"); + + searchBox.value = searchValue; + searchBox.doCommand(); + + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + + for (let i = 0; i < tree.view.rowCount; i++) { + let cellText = tree.view.getCellText(i, tree.columns.getColumnAt(0)); + + if (cellText.includes("example page")) { + found++; + } + } + + info("Reset the search"); + searchBox.value = ""; + searchBox.doCommand(); + + is(found, BOOKMARKS_COUNT, "found expected site"); +} + +async function showInFolder(aSearchStr, aParentFolderGuid) { + let searchBox = sidebar.contentDocument.getElementById("search-box"); + + searchBox.value = aSearchStr; + searchBox.doCommand(); + + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + let theNode = tree.view._getNodeForRow(0); + let bookmarkGuid = theNode.bookmarkGuid; + + Assert.equal(theNode.uri, TEST_SIF_URL, "Found expected bookmark"); + + info("Running Show in Folder command"); + tree.selectNode(theNode); + tree.controller.doCommand("placesCmd_showInFolder"); + + let treeNode = tree.selectedNode; + Assert.equal( + treeNode.parent.bookmarkGuid, + aParentFolderGuid, + "Containing folder node is correct" + ); + Assert.equal( + treeNode.bookmarkGuid, + bookmarkGuid, + "The searched bookmark guid matches selected node" + ); + Assert.equal( + treeNode.uri, + TEST_SIF_URL, + "The searched bookmark URL matches selected node" + ); + + info("Check the title will be applied the item when changing it"); + await PlacesUtils.bookmarks.update({ + guid: theNode.bookmarkGuid, + title: TEST_NEW_TITLE, + }); + Assert.equal( + treeNode.title, + TEST_NEW_TITLE, + "New title is applied to the node" + ); +} + +add_task(async function testTree() { + // Add bookmarks and tags. + for (let i = 0; i < BOOKMARKS_COUNT; i++) { + let url = Services.io.newURI(TEST_URI + i); + + await PlacesUtils.bookmarks.insert({ + url, + title: "example page " + i, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + PlacesUtils.tagging.tagURI(url, ["test"]); + } + + await withSidebarTree("bookmarks", function () { + // Search a bookmark by its title. + assertBookmarks("example.com"); + // Search a bookmark by its tag. + assertBookmarks("test"); + }); + + // Cleanup before testing Show in Folder. + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function testShowInFolder() { + // Now test Show in Folder + let parentFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: TEST_PARENT_FOLDER, + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: parentFolder.guid, + title: TEST_SIF_TITLE, + url: TEST_SIF_URL, + }); + + await withSidebarTree("bookmarks", async function () { + await showInFolder(TEST_SIF_TITLE, parentFolder.guid); + }); + + // Cleanup + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function testRenameOnQueryResult() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: TEST_SIF_TITLE, + url: TEST_SIF_URL, + }); + + await withSidebarTree("bookmarks", async function () { + const searchBox = sidebar.contentDocument.getElementById("search-box"); + + searchBox.value = TEST_SIF_TITLE; + searchBox.doCommand(); + + const tree = sidebar.contentDocument.getElementById("bookmarks-view"); + const theNode = tree.view._getNodeForRow(0); + + info("Check the found bookmark"); + Assert.equal(theNode.uri, TEST_SIF_URL, "URI of bookmark found is correct"); + Assert.equal( + theNode.title, + TEST_SIF_TITLE, + "Title of bookmark found is correct" + ); + + info("Check the title will be applied the item when changing it"); + await PlacesUtils.bookmarks.update({ + guid: theNode.bookmarkGuid, + title: TEST_NEW_TITLE, + }); + + // As the query result is refreshed once then the node also is regenerated, + // need to get the result node from the tree again. + Assert.equal( + tree.view._getNodeForRow(0).bookmarkGuid, + theNode.bookmarkGuid, + "GUID of node regenerated is correct" + ); + Assert.equal( + tree.view._getNodeForRow(0).uri, + theNode.uri, + "URI of node regenerated is correct" + ); + Assert.equal( + tree.view._getNodeForRow(0).parentGuid, + theNode.parentGuid, + "parentGuid of node regenerated is correct" + ); + Assert.equal( + tree.view._getNodeForRow(0).title, + TEST_NEW_TITLE, + "New title is applied to the node" + ); + + info("Check the new date will be applied the item when changing it"); + const now = new Date(); + await PlacesUtils.bookmarks.update({ + guid: theNode.bookmarkGuid, + dateAdded: now, + lastModified: now, + }); + + Assert.equal( + tree.view._getNodeForRow(0).uri, + theNode.uri, + "URI of node regenerated is correct" + ); + Assert.equal( + tree.view._getNodeForRow(0).dateAdded, + now.getTime() * 1000, + "New dateAdded is applied to the node" + ); + Assert.equal( + tree.view._getNodeForRow(0).lastModified, + now.getTime() * 1000, + "New lastModified is applied to the node" + ); + }); + + // Cleanup before testing Show in Folder. + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js b/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js new file mode 100644 index 0000000000..6e1f5e93c2 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testPopup() { + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://example.com", + title: "firefox", + }); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.remove(bookmark); + Services.prefs.clearUserPref("browser.toolbars.bookmarks.visibility"); + }); + + for (let state of ["always", "newtab"]) { + info(`Testing with state set to '${state}'`); + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.bookmarks.visibility", state]], + }); + + let newtab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:newtab", + waitForLoad: false, + }); + + let bookmarksToolbar = document.getElementById("PersonalToolbar"); + await TestUtils.waitForCondition( + () => !bookmarksToolbar.collapsed, + "Wait for toolbar to become visible" + ); + ok(!bookmarksToolbar.collapsed, "Bookmarks toolbar should be visible"); + + // 1. Right-click on a bookmark and check that the submenu is visible + let bookmarkItem = bookmarksToolbar.querySelector( + `.bookmark-item[label="firefox"]` + ); + ok(bookmarkItem, "Got bookmark"); + let contextMenu = document.getElementById("placesContext"); + let popup = await openContextMenu(contextMenu, bookmarkItem); + ok( + !popup.target.querySelector("#toggle_PersonalToolbar").hidden, + "Bookmarks toolbar submenu should appear on a .bookmark-item" + ); + contextMenu.hidePopup(); + + // 2. Right-click on the empty area and check that the submenu is visible + popup = await openContextMenu(contextMenu, bookmarksToolbar); + ok( + !popup.target.querySelector("#toggle_PersonalToolbar").hidden, + "Bookmarks toolbar submenu should appear on the empty part of the toolbar" + ); + + let bookmarksToolbarMenu = document.querySelector( + "#toggle_PersonalToolbar" + ); + let subMenu = bookmarksToolbarMenu.querySelector("menupopup"); + bookmarksToolbarMenu.openMenu(true); + await BrowserTestUtils.waitForPopupEvent(subMenu, "shown"); + let menuitems = subMenu.querySelectorAll("menuitem"); + for (let menuitem of menuitems) { + let expected = menuitem.dataset.visibilityEnum == state; + is( + menuitem.getAttribute("checked"), + expected.toString(), + `The corresponding menuitem, ${menuitem.dataset.visibilityEnum}, ${ + expected ? "should" : "shouldn't" + } be checked if state=${state}` + ); + } + + contextMenu.hidePopup(); + + BrowserTestUtils.removeTab(newtab); + } +}); + +function openContextMenu(contextMenu, target) { + let popupPromise = BrowserTestUtils.waitForPopupEvent(contextMenu, "shown"); + EventUtils.synthesizeMouseAtCenter(target, { + type: "contextmenu", + button: 2, + }); + return popupPromise; +} diff --git a/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js b/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js new file mode 100644 index 0000000000..241710da3b --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const SCALAR_NAME = "browser.ui.customized_widgets"; +const bookmarksInfo = [ + { + title: "firefox", + url: "http://example.com", + }, + { + title: "rules", + url: "http://example.com/2", + }, + { + title: "yo", + url: "http://example.com/2", + }, +]; + +// Setup. +add_task(async function test_bookmarks_toolbar_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.bookmarks.visibility", "newtab"]], + }); + + // This is added during startup + await TestUtils.waitForCondition( + () => + keyedScalarExists( + "browser.ui.toolbar_widgets", + "bookmarks-bar_pinned_newtab", + true + ), + `Waiting for "bookmarks-bar_pinned_newtab" to appear in Telemetry snapshot` + ); + ok(true, `"bookmarks-bar_pinned_newtab"=true found in Telemetry`); + + await changeToolbarVisibilityViaContextMenu("never"); + await assertUIChange( + "bookmarks-bar_move_newtab_never_toolbar-context-menu", + 1 + ); + + await changeToolbarVisibilityViaContextMenu("newtab"); + await assertUIChange( + "bookmarks-bar_move_never_newtab_toolbar-context-menu", + 1 + ); + + await changeToolbarVisibilityViaContextMenu("always"); + await assertUIChange( + "bookmarks-bar_move_newtab_always_toolbar-context-menu", + 1 + ); + + // Extra windows are opened to make sure telemetry numbers aren't + // double counted since there will be multiple instances of the + // bookmarks toolbar. + let extraWindows = []; + extraWindows.push(await BrowserTestUtils.openNewBrowserWindow()); + + Services.telemetry.getSnapshotForScalars("main", true); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: bookmarksInfo, + }); + registerCleanupFunction(() => PlacesUtils.bookmarks.remove(bookmarks)); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + "browser.engagement.bookmarks_toolbar_bookmark_added", + 3, + "Bookmarks added value should be 3" + ); + + let newtab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + registerCleanupFunction(() => BrowserTestUtils.removeTab(newtab)); + let bookmarkToolbarButton = document.querySelector( + "#PlacesToolbarItems > toolbarbutton" + ); + bookmarkToolbarButton.click(); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + "browser.engagement.bookmarks_toolbar_bookmark_opened", + 1, + "Bookmarks opened value should be 1" + ); + + extraWindows.push(await BrowserTestUtils.openNewBrowserWindow()); + extraWindows.push(await BrowserTestUtils.openNewBrowserWindow()); + + // Simulate dragging a bookmark within the toolbar to ensure + // that the bookmarks_toolbar_bookmark_added probe doesn't increment + let srcElement = document.querySelector("#PlacesToolbar .bookmark-item"); + let destElement = document.querySelector("#PlacesToolbar"); + await EventUtils.synthesizePlainDragAndDrop({ + srcElement, + destElement, + }); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + "browser.engagement.bookmarks_toolbar_bookmark_added", + 3, + "Bookmarks added value should still be 3" + ); + + for (let win of extraWindows) { + await BrowserTestUtils.closeWindow(win); + } +}); + +async function changeToolbarVisibilityViaContextMenu(nextState) { + let contextMenu = document.querySelector("#toolbar-context-menu"); + let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + let menuButton = document.getElementById("PanelUI-menu-button"); + EventUtils.synthesizeMouseAtCenter( + menuButton, + { type: "contextmenu" }, + window + ); + await popupShown; + let bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar"); + let subMenu = bookmarksToolbarMenu.querySelector("menupopup"); + popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown"); + bookmarksToolbarMenu.openMenu(true); + await popupShown; + let menuItem = document.querySelector( + `menuitem[data-visibility-enum="${nextState}"]` + ); + subMenu.activateItem(menuItem); + contextMenu.hidePopup(); +} + +async function assertUIChange(key, value) { + await TestUtils.waitForCondition( + () => keyedScalarExists(SCALAR_NAME, key, value), + `Waiting for ${key} to appear in Telemetry snapshot` + ); + ok(true, `${key}=${value} found in Telemetry`); +} + +function keyedScalarExists(scalar, key, value) { + let snapshot = Services.telemetry.getSnapshotForKeyedScalars( + "main", + false + ).parent; + if (!snapshot.hasOwnProperty(scalar)) { + return false; + } + if (!snapshot[scalar].hasOwnProperty(key)) { + info(`Looking for ${key} in ${JSON.stringify(snapshot[scalar])}`); + return false; + } + return snapshot[scalar][key] == value; +} diff --git a/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js b/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js new file mode 100644 index 0000000000..6acb6fb04c --- /dev/null +++ b/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js @@ -0,0 +1,50 @@ +"use strict"; + +/** + * Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if + * insertionPoint is invalid. + */ + +const TEST_URL = "about:buildconfig"; + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + bookmarkPanel.removeAttribute("animate"); + await BrowserTestUtils.removeTab(tab); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + StarUI._createPanelIfNeeded(); + let bookmarkPanel = document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + + let shownPromise = promisePopupShown(bookmarkPanel); + + let bookmarkStar = BookmarkingUI.star; + bookmarkStar.click(); + + await shownPromise; + + ok(gEditItemOverlay, "gEditItemOverlay is in context"); + ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized"); + + window.gEditItemOverlay.toggleFolderTreeVisibility(); + + let tree = gEditItemOverlay._element("folderTree"); + + tree.view.selection.clearSelection(); + ok( + document.getElementById("editBMPanel_newFolderButton").disabled, + "New folder button is disabled if there's no selection" + ); + + let hiddenPromise = promisePopupHidden(bookmarkPanel); + document.getElementById("editBookmarkPanelRemoveButton").click(); + await hiddenPromise; +}); diff --git a/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js new file mode 100644 index 0000000000..9abb82c9bb --- /dev/null +++ b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js @@ -0,0 +1,78 @@ +"use strict"; + +const TEST_URL = "https://www.example.com/"; +const testTag = "foo"; +const testTagUpper = "Foo"; +const testURI = Services.io.newURI(TEST_URL); + +add_task(async function test() { + // Add a bookmark. + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: "mozilla", + url: testURI, + }); + + PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["tag0"]); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + }); + win.StarUI._createPanelIfNeeded(); + + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: TEST_URL, + }, + async () => { + // Init panel + await TestUtils.waitForCondition( + () => BookmarkingUI.status !== BookmarkingUI.STATUS_UPDATING + ); + + await clickBookmarkStar(win); + + // add a tag + await fillBookmarkTextField("editBMPanel_tagsField", testTag, win); + + let promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + await hideBookmarksPanel(win); + await promiseNotification; + + // test that the tag has been added in the backend + is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match"); + + // change the tag + await TestUtils.waitForCondition( + () => BookmarkingUI.status !== BookmarkingUI.STATUS_UPDATING + ); + await clickBookmarkStar(win); + await fillBookmarkTextField("editBMPanel_tagsField", testTagUpper, win); + // The old sync API doesn't notify a tags change, and fixing it would be + // quite complex, so we just wait for a title change until tags are + // refactored. + promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-title-changed" + ); + await hideBookmarksPanel(win); + await promiseNotification; + + // test that the tag has been added in the backend + is( + PlacesUtils.tagging.getTagsForURI(testURI)[0], + testTagUpper, + "tags match" + ); + + // Cleanup. + PlacesUtils.tagging.untagURI(testURI, [testTag]); + await PlacesUtils.bookmarks.remove(bm.guid); + } + ); +}); diff --git a/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js b/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js new file mode 100644 index 0000000000..5d35356a69 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js @@ -0,0 +1,162 @@ +/** + * This test checks that editing tags doesn't scroll the tags selector + * listbox to wrong positions. + */ + +const TEST_URL = "about:buildconfig"; + +function scrolledIntoView(item, parentItem) { + let itemRect = item.getBoundingClientRect(); + let parentItemRect = parentItem.getBoundingClientRect(); + let pointInView = y => parentItemRect.top < y && y < parentItemRect.bottom; + + // Partially visible items are also considered visible. + return pointInView(itemRect.top) || pointInView(itemRect.bottom); +} + +add_task(async function runTest() { + await PlacesUtils.bookmarks.eraseEverything(); + let tags = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "l", + "m", + "n", + "o", + "p", + ]; + + // Add a bookmark and tag it. + let uri1 = Services.io.newURI(TEST_URL); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "mozilla", + url: uri1.spec, + }); + PlacesUtils.tagging.tagURI(uri1, tags); + + // Add a second bookmark so that tags won't disappear when unchecked. + let uri2 = Services.io.newURI("http://www2.mozilla.org/"); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "mozilla", + url: uri2.spec, + }); + PlacesUtils.tagging.tagURI(uri2, tags); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + registerCleanupFunction(async () => { + bookmarkPanel.removeAttribute("animate"); + await BrowserTestUtils.closeWindow(win); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + win.StarUI._createPanelIfNeeded(); + let bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + let shownPromise = promisePopupShown(bookmarkPanel); + + let bookmarkStar = win.BookmarkingUI.star; + bookmarkStar.click(); + + await shownPromise; + + // Init panel. + ok(win.gEditItemOverlay, "gEditItemOverlay is in context"); + ok(win.gEditItemOverlay.initialized, "gEditItemOverlay is initialized"); + + await openTagSelector(win); + let tagsSelector = win.document.getElementById("editBMPanel_tagsSelector"); + + // Go by two so there is some untouched tag in the middle. + for (let i = 8; i < tags.length; i += 2) { + tagsSelector.selectedIndex = i; + let listItem = tagsSelector.selectedItem; + isnot(listItem, null, "Valid listItem found"); + + tagsSelector.ensureElementIsVisible(listItem); + let scrollTop = tagsSelector.scrollTop; + + ok(listItem.hasAttribute("checked"), "Item is checked " + i); + let selectedTag = listItem.label; + + // Uncheck the tag. + let promise = BrowserTestUtils.waitForEvent( + tagsSelector, + "BookmarkTagsSelectorUpdated" + ); + EventUtils.synthesizeMouseAtCenter(listItem.firstElementChild, {}, win); + await promise; + is(scrollTop, tagsSelector.scrollTop, "Scroll position did not change"); + + // The listbox is rebuilt, so we have to get the new element. + let newItem = tagsSelector.selectedItem; + isnot(newItem, null, "Valid new listItem found"); + ok(!newItem.hasAttribute("checked"), "New listItem is unchecked " + i); + is(newItem.label, selectedTag, "Correct tag is still selected"); + + // Check the tag. + promise = BrowserTestUtils.waitForEvent( + tagsSelector, + "BookmarkTagsSelectorUpdated" + ); + EventUtils.synthesizeMouseAtCenter(newItem.firstElementChild, {}, win); + await promise; + is(scrollTop, tagsSelector.scrollTop, "Scroll position did not change"); + } + + // Remove the second bookmark, then nuke some of the tags. + await PlacesUtils.bookmarks.remove(bm2); + + // Allow the tag updates to complete + await PlacesTestUtils.promiseAsyncUpdates(); + + // Doing this backwords tests more interesting paths. + for (let i = tags.length - 1; i >= 0; i -= 2) { + tagsSelector.selectedIndex = i; + let listItem = tagsSelector.selectedItem; + isnot(listItem, null, "Valid listItem found"); + + tagsSelector.ensureElementIsVisible(listItem); + + ok(listItem.hasAttribute("checked"), "Item is checked " + i); + + // Uncheck the tag. + let promise = BrowserTestUtils.waitForEvent( + tagsSelector, + "BookmarkTagsSelectorUpdated" + ); + EventUtils.synthesizeMouseAtCenter(listItem.firstElementChild, {}, win); + await promise; + } + + let hiddenPromise = promisePopupHidden(bookmarkPanel); + let doneButton = win.document.getElementById("editBookmarkPanelDoneButton"); + doneButton.click(); + await hiddenPromise; + // Cleanup. + await PlacesUtils.bookmarks.remove(bm1); +}); + +function openTagSelector(win) { + let promise = BrowserTestUtils.waitForEvent( + win.document.getElementById("editBMPanel_tagsSelector"), + "BookmarkTagsSelectorUpdated" + ); + // Open the tags selector. + win.document.getElementById("editBMPanel_tagsSelectorExpander").doCommand(); + return promise; +} diff --git a/browser/components/places/tests/browser/browser_check_correct_controllers.js b/browser/components/places/tests/browser/browser_check_correct_controllers.js new file mode 100644 index 0000000000..80095823e1 --- /dev/null +++ b/browser/components/places/tests/browser/browser_check_correct_controllers.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_setup(async () => { + // Ensure all bookmarks cleared before the test starts. + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test() { + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Plain Bob", + url: "http://example.com", + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.remove(bookmark); + }); + + let sidebarBox = document.getElementById("sidebar-box"); + is(sidebarBox.hidden, true, "The sidebar should be hidden"); + + // Uncollapse the personal toolbar if needed. + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + let sidebar = await promiseLoadedSidebar("viewBookmarksSidebar"); + registerCleanupFunction(() => { + SidebarUI.hide(); + }); + + // Focus the tree and check if its controller is returned. + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + tree.focus(); + + let controller = PlacesUIUtils.getControllerForCommand( + window, + "placesCmd_copy" + ); + let treeController = + tree.controllers.getControllerForCommand("placesCmd_copy"); + Assert.equal(controller, treeController, "tree controller was returned"); + + // Open the context menu for a toolbar item, and check if the toolbar's + // controller is returned. + let toolbarItems = document.getElementById("PlacesToolbarItems"); + // Ensure the toolbar has displayed the bookmark. This might be async, so + // wait a little if necessary. + await TestUtils.waitForCondition( + () => toolbarItems.children.length == 1, + "Should have only one item on the toolbar" + ); + + let placesContext = document.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouse( + toolbarItems.children[0], + 4, + 4, + { type: "contextmenu", button: 2 }, + window + ); + await popupShownPromise; + + controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy"); + let toolbarController = document + .getElementById("PlacesToolbar") + .controllers.getControllerForCommand("placesCmd_copy"); + Assert.equal( + controller, + toolbarController, + "the toolbar controller was returned" + ); + + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + placesContext, + "popuphidden" + ); + placesContext.hidePopup(); + await popupHiddenPromise; + + // Now that the context menu is closed, try to get the tree controller again. + tree.focus(); + controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy"); + Assert.equal(controller, treeController, "tree controller was returned"); + + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } +}); + +function promiseLoadedSidebar(cmd) { + return new Promise(resolve => { + let sidebar = document.getElementById("sidebar"); + sidebar.addEventListener( + "load", + function () { + executeSoon(() => resolve(sidebar)); + }, + { capture: true, once: true } + ); + + SidebarUI.show(cmd); + }); +} diff --git a/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js new file mode 100644 index 0000000000..5ecf95d94e --- /dev/null +++ b/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js @@ -0,0 +1,234 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs"; +const EXAMPLE_PAGE = "http://example.com/"; +const TEST_PAGES = [ + "about:mozilla", + "about:robots", + "javascript:window.location=%22" + EXAMPLE_PAGE + "%22", +]; + +var gBookmarkElements = []; + +function waitForBookmarkElements(expectedCount) { + let container = document.getElementById("PlacesToolbarItems"); + if (container.childElementCount == expectedCount) { + return Promise.resolve(); + } + return new Promise(resolve => { + info("Waiting for bookmarks"); + let mut = new MutationObserver(mutations => { + info("Elements appeared"); + if (container.childElementCount == expectedCount) { + resolve(); + mut.disconnect(); + } + }); + + mut.observe(container, { childList: true }); + }); +} + +function getToolbarNodeForItemGuid(aItemGuid) { + var children = document.getElementById("PlacesToolbarItems").children; + for (let child of children) { + if (aItemGuid == child._placesNode.bookmarkGuid) { + return child; + } + } + return null; +} + +function waitForLoad(browser, url) { + return BrowserTestUtils.browserLoaded(browser, false, url); +} + +function waitForNewTab(url, inBackground) { + return BrowserTestUtils.waitForNewTab(gBrowser, url).then(tab => { + if (inBackground) { + Assert.notEqual( + tab, + gBrowser.selectedTab, + `The new tab is in the background` + ); + } else { + Assert.equal( + tab, + gBrowser.selectedTab, + `The new tab is in the foreground` + ); + } + + BrowserTestUtils.removeTab(tab); + }); +} + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + let bookmarks = await Promise.all( + TEST_PAGES.map((url, index) => { + return PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: `Title ${index}`, + url, + }); + }) + ); + + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + await waitForBookmarkElements(TEST_PAGES.length); + for (let bookmark of bookmarks) { + let element = getToolbarNodeForItemGuid(bookmark.guid); + Assert.notEqual(element, null, "Found node on toolbar"); + + gBookmarkElements.push(element); + } + + registerCleanupFunction(async () => { + gBookmarkElements = []; + + await Promise.all( + bookmarks.map(bookmark => { + return PlacesUtils.bookmarks.remove(bookmark); + }) + ); + + // Note: hiding the toolbar before removing the bookmarks triggers + // bug 1766284. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + }); +}); + +add_task(async function click() { + let promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[0]); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], { + button: 0, + }); + await promise; + + promise = waitForNewTab(TEST_PAGES[1], false); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], { + button: 0, + accelKey: true, + }); + await promise; + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:blank", + }); + promise = waitForLoad(gBrowser.selectedBrowser, EXAMPLE_PAGE); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[2], {}); + await promise; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function middleclick() { + let promise = waitForNewTab(TEST_PAGES[0], true); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], { + button: 1, + shiftKey: true, + }); + await promise; + + promise = waitForNewTab(TEST_PAGES[1], false); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], { + button: 1, + }); + await promise; +}); + +add_task(async function clickWithPrefSet() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]], + }); + + let promise = waitForNewTab(TEST_PAGES[0], false); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], { + button: 0, + }); + await promise; + + // With loadBookmarksInTabs, reuse current tab if blank + for (let button of [0, 1]) { + await BrowserTestUtils.withNewTab({ gBrowser }, async tab => { + promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[1]); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], { + button, + }); + await promise; + }); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function openInSameTabWithPrefSet() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]], + }); + + let placesContext = document.getElementById("placesContext"); + let popupPromise = BrowserTestUtils.waitForEvent(placesContext, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], { + button: 2, + type: "contextmenu", + }); + info("Waiting for context menu"); + await popupPromise; + + let openItem = document.getElementById("placesContext_open"); + ok(BrowserTestUtils.isVisible(openItem), "Open item should be visible"); + + info("Waiting for page to load"); + let promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[0]); + openItem.doCommand(); + placesContext.hidePopup(); + await promise; + + await SpecialPowers.popPrefEnv(); +}); + +// Open a tab, then quickly open the context menu to ensure that the command +// enabled state of the menuitems is updated properly. +add_task(async function quickContextMenu() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]], + }); + + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TEST_PAGES[0]); + + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], { + button: 0, + }); + let newTab = await tabPromise; + + let placesContext = document.getElementById("placesContext"); + let promise = BrowserTestUtils.waitForEvent(placesContext, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], { + button: 2, + type: "contextmenu", + }); + await promise; + + Assert.ok( + !document.getElementById("placesContext_open:newtab").disabled, + "Commands in context menu are enabled" + ); + + promise = BrowserTestUtils.waitForEvent(placesContext, "popuphidden"); + placesContext.hidePopup(); + await promise; + BrowserTestUtils.removeTab(newTab); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/places/tests/browser/browser_controller_onDrop.js b/browser/components/places/tests/browser/browser_controller_onDrop.js new file mode 100644 index 0000000000..cbda2612cf --- /dev/null +++ b/browser/components/places/tests/browser/browser_controller_onDrop.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var bookmarks; +var bookmarkIds; +var library; + +add_setup(async function () { + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await promiseLibraryClosed(library); + }); + + bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "http://example1.com", + }, + { + title: "bm2", + url: "http://example2.com", + }, + { + title: "bm3", + url: "http://example3.com", + }, + ], + }); + + bookmarkIds = await PlacesTestUtils.promiseManyItemIds([ + bookmarks[0].guid, + bookmarks[1].guid, + bookmarks[2].guid, + ]); + + library = await promiseLibrary("UnfiledBookmarks"); +}); + +async function run_drag_test(startBookmarkIndex, insertionIndex) { + let dragBookmark = bookmarks[startBookmarkIndex]; + + library.ContentTree.view.selectItems([dragBookmark.guid]); + + let dataTransfer = { + _data: [], + dropEffect: "move", + mozCursor: "auto", + mozItemCount: 1, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return [this._data[0].type]; + }, + mozGetDataAt(i) { + return this._data[0].data; + }, + mozSetDataAt(type, data, index) { + this._data.push({ + type, + data, + index, + }); + }, + }; + + let event = { + dataTransfer, + preventDefault() {}, + stopPropagation() {}, + }; + + library.ContentTree.view.controller.setDataTransfer(event); + + Assert.equal( + dataTransfer.mozTypesAt(0), + PlacesUtils.TYPE_X_MOZ_PLACE, + "Should have x-moz-place as the first data type." + ); + + let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0)); + + Assert.equal( + dataObject.itemGuid, + dragBookmark.guid, + "Should have the correct guid." + ); + Assert.equal( + dataObject.title, + dragBookmark.title, + "Should have the correct title." + ); + + Assert.equal(dataTransfer.dropEffect, "move"); + + let ip = new PlacesInsertionPoint({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: insertionIndex, + orientation: Ci.nsITreeView.DROP_ON, + }); + + await PlacesControllerDragHelper.onDrop(ip, dataTransfer); +} + +add_task(async function test_simple_move_down() { + let moveNotification = PlacesTestUtils.waitForNotification( + "bookmark-moved", + events => + events.some( + e => e.guid === bookmarks[0].guid && e.oldIndex == 0 && e.index == 1 + ) + ); + + await run_drag_test(0, 2); + + await moveNotification; +}); + +add_task(async function test_simple_move_up() { + await run_drag_test(2, 0); +}); diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_query.js b/browser/components/places/tests/browser/browser_controller_onDrop_query.js new file mode 100644 index 0000000000..10dd6faa3c --- /dev/null +++ b/browser/components/places/tests/browser/browser_controller_onDrop_query.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "about:buildconfig"; + +add_setup(async function () { + // Clean before and after so we don't have anything in the folders. + await PlacesUtils.bookmarks.eraseEverything(); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Simulating actual drag and drop is hard for a xul tree as we can't get the +// required source elements, so we have to do a lot more work by hand. +async function simulateDrop( + selectTargets, + sourceBm, + dropEffect, + targetGuid, + isVirtualRoot = false +) { + await withSidebarTree("bookmarks", async function (tree) { + for (let target of selectTargets) { + tree.selectItems([target]); + if (tree.selectedNode instanceof Ci.nsINavHistoryContainerResultNode) { + tree.selectedNode.containerOpen = true; + } + } + + let dataTransfer = { + _data: [], + dropEffect, + mozCursor: "auto", + mozItemCount: 1, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return [this._data[0].type]; + }, + mozGetDataAt(i) { + return this._data[0].data; + }, + mozSetDataAt(type, data, index) { + this._data.push({ + type, + data, + index, + }); + }, + }; + + let event = { + dataTransfer, + preventDefault() {}, + stopPropagation() {}, + }; + + tree._controller.setDataTransfer(event); + + Assert.equal( + dataTransfer.mozTypesAt(0), + PlacesUtils.TYPE_X_MOZ_PLACE, + "Should have x-moz-place as the first data type." + ); + + let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0)); + + let guid = isVirtualRoot ? dataObject.concreteGuid : dataObject.itemGuid; + + Assert.equal(guid, sourceBm.guid, "Should have the correct guid."); + Assert.equal( + dataObject.title, + PlacesUtils.bookmarks.getLocalizedTitle(sourceBm), + "Should have the correct title." + ); + + Assert.equal(dataTransfer.dropEffect, dropEffect); + + let ip = new PlacesInsertionPoint({ + parentId: await PlacesTestUtils.promiseItemId(targetGuid), + parentGuid: targetGuid, + index: 0, + orientation: Ci.nsITreeView.DROP_ON, + }); + + await PlacesControllerDragHelper.onDrop(ip, dataTransfer); + }); +} + +add_task(async function test_move_out_of_query() { + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Fake", + url: TEST_URL, + }); + + let queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + let queries = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + title: "Query", + url: `place:queryType=${queryType}&terms=Fake`, + }, + ], + }); + await simulateDrop( + [queries[0].guid, bookmark.guid], + bookmark, + "move", + PlacesUtils.bookmarks.toolbarGuid + ); + + let newBm = await PlacesUtils.bookmarks.fetch(bookmark.guid); + + Assert.equal( + newBm.parentGuid, + PlacesUtils.bookmarks.toolbarGuid, + "should have moved the bookmark to a new folder." + ); + + let oldLocationBm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + + Assert.ok(!oldLocationBm, "Should not have a bookmark at the old location."); +}); diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js b/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js new file mode 100644 index 0000000000..2637d4d724 --- /dev/null +++ b/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js @@ -0,0 +1,312 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "about:buildconfig"; + +add_setup(async function () { + // The following initialization code is necessary to avoid a frequent + // intermittent failure in verify-fission where, due to timings, we may or + // may not import default bookmarks. + info("Ensure Places init is complete"); + let placesInitCompleteObserved = TestUtils.topicObserved( + "places-browser-init-complete" + ); + Cc["@mozilla.org/browser/browserglue;1"] + .getService(Ci.nsIObserver) + .observe(null, "browser-glue-test", "places-browser-init-complete"); + await placesInitCompleteObserved; + // Clean before and after so we don't have anything in the folders. + await PlacesUtils.bookmarks.eraseEverything(); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Simulating actual drag and drop is hard for a xul tree as we can't get the +// required source elements, so we have to do a lot more work by hand. +async function simulateDrop( + selectTargets, + sourceBm, + dropEffect, + targetGuid, + isVirtualRoot = false +) { + await withSidebarTree("bookmarks", async function (tree) { + for (let target of selectTargets) { + tree.selectItems([target]); + if (tree.selectedNode instanceof Ci.nsINavHistoryContainerResultNode) { + tree.selectedNode.containerOpen = true; + } + } + + let dataTransfer = { + _data: [], + dropEffect, + mozCursor: "auto", + mozItemCount: 1, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return [this._data[0].type]; + }, + mozGetDataAt(i) { + return this._data[0].data; + }, + mozSetDataAt(type, data, index) { + this._data.push({ + type, + data, + index, + }); + }, + }; + + let event = { + dataTransfer, + preventDefault() {}, + stopPropagation() {}, + }; + + tree._controller.setDataTransfer(event); + + Assert.equal( + dataTransfer.mozTypesAt(0), + PlacesUtils.TYPE_X_MOZ_PLACE, + "Should have x-moz-place as the first data type." + ); + + let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0)); + + let guid = isVirtualRoot ? dataObject.concreteGuid : dataObject.itemGuid; + + Assert.equal(guid, sourceBm.guid, "Should have the correct guid."); + Assert.equal( + dataObject.title, + PlacesUtils.bookmarks.getLocalizedTitle(sourceBm), + "Should have the correct title." + ); + + Assert.equal(dataTransfer.dropEffect, dropEffect); + + let ip = new PlacesInsertionPoint({ + parentId: await PlacesTestUtils.promiseItemId(targetGuid), + parentGuid: targetGuid, + index: 0, + orientation: Ci.nsITreeView.DROP_ON, + }); + + await PlacesControllerDragHelper.onDrop(ip, dataTransfer); + }); +} + +add_task(async function test_move_normal_bm_in_sidebar() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Fake", + url: TEST_URL, + }); + + await simulateDrop([bm.guid], bm, "move", PlacesUtils.bookmarks.unfiledGuid); + + let newBm = await PlacesUtils.bookmarks.fetch(bm.guid); + + Assert.equal( + newBm.parentGuid, + PlacesUtils.bookmarks.unfiledGuid, + "Should have moved to the new parent." + ); + + let oldLocationBm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + + Assert.ok(!oldLocationBm, "Should not have a bookmark at the old location."); +}); + +add_task(async function test_try_move_root_in_sidebar() { + let menuFolder = await PlacesUtils.bookmarks.fetch( + PlacesUtils.bookmarks.menuGuid + ); + await simulateDrop( + [menuFolder.guid], + menuFolder, + "move", + PlacesUtils.bookmarks.toolbarGuid, + true + ); + + menuFolder = await PlacesUtils.bookmarks.fetch( + PlacesUtils.bookmarks.menuGuid + ); + + Assert.equal( + menuFolder.parentGuid, + PlacesUtils.bookmarks.rootGuid, + "Should have remained in the root" + ); + + let newFolder = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + + Assert.notEqual( + newFolder.guid, + menuFolder.guid, + "Should have created a different folder" + ); + Assert.equal( + newFolder.title, + PlacesUtils.bookmarks.getLocalizedTitle(menuFolder), + "Should have copied the folder title." + ); + Assert.equal( + newFolder.type, + PlacesUtils.bookmarks.TYPE_BOOKMARK, + "Should have a bookmark type (for a folder shortcut)." + ); + Assert.equal( + newFolder.url, + `place:parent=${PlacesUtils.bookmarks.menuGuid}`, + "Should have the correct url for the folder shortcut." + ); +}); + +add_task(async function test_try_move_bm_within_two_root_folder_queries() { + await PlacesUtils.bookmarks.eraseEverything(); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Fake", + url: TEST_URL, + }); + + let queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + let queries = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + title: "Query", + url: `place:queryType=${queryType}&terms=Fake`, + }, + ], + }); + + await simulateDrop( + [queries[0].guid, bookmark.guid], + bookmark, + "move", + PlacesUtils.bookmarks.toolbarGuid + ); + + let newBm = await PlacesUtils.bookmarks.fetch(bookmark.guid); + + Assert.equal( + newBm.parentGuid, + PlacesUtils.bookmarks.toolbarGuid, + "should have moved the bookmark to a new folder." + ); +}); + +add_task(async function test_move_within_itself() { + await PlacesUtils.bookmarks.eraseEverything(); + + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "http://www.example.com/bookmark1.html", + }, + { + title: "bm2", + url: "http://www.example.com/bookmark2.html", + }, + { + title: "bm3", + url: "http://www.example.com/bookmark3.html", + }, + ], + }); + + await withSidebarTree("bookmarks", async function (tree) { + // Select the folder containing the bookmarks + // and save its index position + tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]); + let unfiledFolderIndex = tree.view.treeIndexForNode(tree.selectedNode); + + let guids = bookmarks.map(bookmark => bookmark.guid); + tree.selectItems(guids); + let dataTransfer = { + _data: [], + dropEffect: "move", + mozCursor: "auto", + mozItemCount: bookmarks.length, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return [this._data[0].type]; + }, + mozGetDataAt(i) { + return this._data[0].data; + }, + mozSetDataAt(type, data, index) { + this._data.push({ + type, + data, + index, + }); + }, + }; + + bookmarks.forEach((bookmark, index) => { + // Index positions of the newly created bookmarks + bookmark.rowIndex = unfiledFolderIndex + index + 1; + bookmark.node = tree.view.nodeForTreeIndex(bookmark.rowIndex); + bookmark.cachedBookmarkIndex = bookmark.node.bookmarkIndex; + }); + + let assertBookmarksHaveNotChangedPosition = () => { + bookmarks.forEach(bookmark => { + Assert.equal( + bookmark.node.bookmarkIndex, + bookmark.cachedBookmarkIndex, + "should not have moved the bookmark." + ); + }); + }; + + // Mimic "drag" events + let dragStartEvent = new CustomEvent("dragstart", { + bubbles: true, + }); + dragStartEvent.dataTransfer = dataTransfer; + + let dragEndEvent = new CustomEvent("dragend", { + bubbles: true, + }); + + let treeChildren = tree.view._element.children[1]; + + treeChildren.dispatchEvent(dragStartEvent); + await tree.view.drop( + bookmarks[1].rowIndex, + Ci.nsITreeView.DROP_ON, + dataTransfer + ); + treeChildren.dispatchEvent(dragEndEvent); + assertBookmarksHaveNotChangedPosition(); + + treeChildren.dispatchEvent(dragStartEvent); + await tree.view.drop( + bookmarks[2].rowIndex, + Ci.nsITreeView.DROP_ON, + dataTransfer + ); + treeChildren.dispatchEvent(dragEndEvent); + assertBookmarksHaveNotChangedPosition(); + }); +}); diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js b/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js new file mode 100644 index 0000000000..64c448ec3f --- /dev/null +++ b/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const sandbox = sinon.createSandbox(); +const TAG_NAME = "testTag"; + +var bookmarks; +var bookmarkId; + +add_setup(async function () { + registerCleanupFunction(async function () { + sandbox.restore(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + }); + + sandbox.stub(PlacesTransactions, "batch"); + sandbox.stub(PlacesTransactions, "Tag"); + + bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "http://example1.com", + }, + { + title: "bm2", + url: "http://example2.com", + tags: [TAG_NAME], + }, + ], + }); + bookmarkId = await PlacesTestUtils.promiseItemId(bookmarks[0].guid); +}); + +async function run_drag_test(startBookmarkIndex, newParentGuid) { + if (!newParentGuid) { + newParentGuid = PlacesUtils.bookmarks.unfiledGuid; + } + + // Reset the stubs so that previous test runs don't count against us. + PlacesTransactions.Tag.reset(); + PlacesTransactions.batch.reset(); + + let dragBookmark = bookmarks[startBookmarkIndex]; + + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + + // Simulating a drag-drop with a tree view turns out to be really difficult + // as you can't get a node for the source/target. Hence, we fake the + // insertion point and drag data and call the function direct. + let ip = new PlacesInsertionPoint({ + isTag: true, + tagName: TAG_NAME, + orientation: Ci.nsITreeView.DROP_ON, + }); + + let bookmarkWithId = JSON.stringify( + Object.assign( + { + id: bookmarkId, + itemGuid: dragBookmark.guid, + uri: dragBookmark.url, + }, + dragBookmark + ) + ); + + let dt = { + dropEffect: "move", + mozCursor: "auto", + mozItemCount: 1, + types: [PlacesUtils.TYPE_X_MOZ_PLACE], + mozTypesAt(i) { + return this.types; + }, + mozGetDataAt(i) { + return bookmarkWithId; + }, + }; + + await PlacesControllerDragHelper.onDrop(ip, dt); + + Assert.ok( + PlacesTransactions.Tag.calledOnce, + "Should have called PlacesTransactions.Tag at least once." + ); + + let arg = PlacesTransactions.Tag.args[0][0]; + + Assert.equal( + arg.urls.length, + 1, + "Should have called PlacesTransactions.Tag with an array of one url" + ); + Assert.equal( + arg.urls[0], + dragBookmark.url, + "Should have called PlacesTransactions.Tag with the correct url" + ); + Assert.equal( + arg.tag, + TAG_NAME, + "Should have called PlacesTransactions.Tag with the correct tag name" + ); + }); +} + +add_task(async function test_simple_drop_and_tag() { + // When we move items down the list, we'll get a drag index that is one higher + // than where we actually want to insert to - as the item is being moved up, + // everything shifts down one. Hence the index to pass to the transaction should + // be one less than the supplied index. + await run_drag_test(0, PlacesUtils.bookmarks.tagGuid); +}); diff --git a/browser/components/places/tests/browser/browser_copy_query_without_tree.js b/browser/components/places/tests/browser/browser_copy_query_without_tree.js new file mode 100644 index 0000000000..fc3adf31f2 --- /dev/null +++ b/browser/components/places/tests/browser/browser_copy_query_without_tree.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* test that copying a non movable query or folder shortcut makes a new query with the same url, not a deep copy */ + +const QUERY_URL = "place:sort=8&maxResults=10"; + +add_task(async function copy_toolbar_shortcut() { + await promisePlacesInitComplete(); + + let library = await promiseLibrary(); + + registerCleanupFunction(async () => { + library.close(); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + await promiseClipboard(function () { + library.PlacesOrganizer._places.controller.copy(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + await library.ContentTree.view.controller.paste(); + + let toolbarCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0); + is( + toolbarCopyNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT, + "copy is still a folder shortcut" + ); + + await PlacesUtils.bookmarks.remove(toolbarCopyNode.bookmarkGuid); + library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + is( + library.PlacesOrganizer._places.selectedNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT, + "original is still a folder shortcut" + ); +}); + +add_task(async function copy_mobile_shortcut() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.bookmarks.showMobileBookmarks", true]], + }); + await promisePlacesInitComplete(); + + let library = await promiseLibrary(); + + registerCleanupFunction(async () => { + library.close(); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + library.PlacesOrganizer.selectLeftPaneContainerByHierarchy([ + PlacesUtils.virtualAllBookmarksGuid, + PlacesUtils.bookmarks.virtualMobileGuid, + ]); + + await promiseClipboard(function () { + library.PlacesOrganizer._places.controller.copy(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + await library.ContentTree.view.controller.paste(); + + let mobileCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0); + is( + mobileCopyNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT, + "copy is still a folder shortcut" + ); + + await PlacesUtils.bookmarks.remove(mobileCopyNode.bookmarkGuid); + library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + is( + library.PlacesOrganizer._places.selectedNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT, + "original is still a folder shortcut" + ); +}); + +add_task(async function copy_history_query() { + let library = await promiseLibrary(); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("History"); + + await promiseClipboard(function () { + library.PlacesOrganizer._places.controller.copy(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + await library.ContentTree.view.controller.paste(); + + let historyCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0); + is( + historyCopyNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY, + "copy is still a query" + ); + + await PlacesUtils.bookmarks.remove(historyCopyNode.bookmarkGuid); + library.PlacesOrganizer.selectLeftPaneBuiltIn("History"); + is( + library.PlacesOrganizer._places.selectedNode.type, + Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY, + "original is still a query" + ); +}); diff --git a/browser/components/places/tests/browser/browser_cutting_bookmarks.js b/browser/components/places/tests/browser/browser_cutting_bookmarks.js new file mode 100644 index 0000000000..7c10229f7e --- /dev/null +++ b/browser/components/places/tests/browser/browser_cutting_bookmarks.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "http://example.com/"; + +add_task(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + let organizer = await promiseLibrary(); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(organizer); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + let PlacesOrganizer = organizer.PlacesOrganizer; + let ContentTree = organizer.ContentTree; + + // Test with multiple entries to ensure they retain their order. + let bookmarks = []; + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: TEST_URL, + title: "0", + }) + ); + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: TEST_URL, + title: "1", + }) + ); + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: TEST_URL, + title: "2", + }) + ); + + await selectBookmarksIn(organizer, bookmarks, "BookmarksToolbar"); + + await promiseClipboard(() => { + info("Cutting selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + await selectBookmarksIn(organizer, bookmarks, "UnfiledBookmarks"); +}); + +var selectBookmarksIn = async function (organizer, bookmarks, aLeftPaneQuery) { + let PlacesOrganizer = organizer.PlacesOrganizer; + let ContentTree = organizer.ContentTree; + info("Selecting " + aLeftPaneQuery + " in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn(aLeftPaneQuery); + + for (let { guid } of bookmarks) { + let bookmark = await PlacesUtils.bookmarks.fetch(guid); + is( + bookmark.parentGuid, + PlacesUtils.getConcreteItemGuid(PlacesOrganizer._places.selectedNode), + "Bookmark has the right parent" + ); + } + + info("Selecting the bookmarks in the right pane"); + ContentTree.view.selectItems(bookmarks.map(bm => bm.guid)); + + for (let node of ContentTree.view.selectedNodes) { + is( + "" + node.bookmarkIndex, + node.title, + "Found the expected bookmark in the expected position" + ); + } +}; diff --git a/browser/components/places/tests/browser/browser_default_bookmark_location.js b/browser/components/places/tests/browser/browser_default_bookmark_location.js new file mode 100644 index 0000000000..9b386fd2f0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_default_bookmark_location.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LOCATION_PREF = "browser.bookmarks.defaultLocation"; +const TEST_URL = "about:about"; +let bookmarkPanel; +let win; + +add_setup(async function () { + Services.prefs.clearUserPref(LOCATION_PREF); + await PlacesUtils.bookmarks.eraseEverything(); + + win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + let oldTimeout = win.StarUI._autoCloseTimeout; + // Make the timeout something big, so it doesn't interact badly with tests. + win.StarUI._autoCloseTimeout = 6000000; + + win.StarUI._createPanelIfNeeded(); + bookmarkPanel = win.document.getElementById("editBookmarkPanel"); + bookmarkPanel.setAttribute("animate", false); + + registerCleanupFunction(async () => { + bookmarkPanel = null; + win.StarUI._autoCloseTimeout = oldTimeout; + await BrowserTestUtils.closeWindow(win); + win = null; + await PlacesUtils.bookmarks.eraseEverything(); + Services.prefs.clearUserPref(LOCATION_PREF); + }); +}); + +async function cancelBookmarkCreationInPanel() { + let hiddenPromise = promisePopupHidden( + win.document.getElementById("editBookmarkPanel") + ); + // Confirm and close the dialog. + + win.document.getElementById("editBookmarkPanelRemoveButton").click(); + await hiddenPromise; +} + +/** + * Helper to check the selected folder is correct. + */ +async function checkSelection() { + // Open folder selector. + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + + let expectedFolder = "BookmarksToolbarFolderTitle"; + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + `Should have ${expectedFolder} selected by default` + ); + Assert.equal( + menuList.getAttribute("selectedGuid"), + await PlacesUIUtils.defaultParentGuid, + "Should have the correct default guid selected" + ); + + await cancelBookmarkCreationInPanel(); +} + +/** + * Verify that bookmarks created with the star button go to the default + * bookmark location. + */ +add_task(async function test_star_location() { + await clickBookmarkStar(win); + await checkSelection(); +}); + +/** + * Verify that bookmarks created with the shortcut go to the default bookmark + * location. + */ +add_task(async function test_shortcut_location() { + let shownPromise = promisePopupShown( + win.document.getElementById("editBookmarkPanel") + ); + win.document.getElementById("Browser:AddBookmarkAs").doCommand(); + await shownPromise; + await checkSelection(); +}); + +// Note: Bookmarking frames is tested in browser_addBookmarkForFrame.js + +/** + * Verify that bookmarks created with the link context menu go to the default + * bookmark location. + */ +add_task(async function test_context_menu_link() { + for (let t = 0; t < 2; t++) { + if (t == 1) { + // For the second iteration, ensure that the default folder is invalid first. + await createAndRemoveDefaultFolder(); + } + + await withBookmarksDialog( + true, + async function openDialog() { + const contextMenu = win.document.getElementById( + "contentAreaContextMenu" + ); + is(contextMenu.state, "closed", "checking if popup is closed"); + let promisePopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "a[href*=config]", // Bookmark about:config + { type: "contextmenu", button: 2 }, + win.gBrowser.selectedBrowser + ); + await promisePopupShown; + contextMenu.activateItem( + win.document.getElementById("context-bookmarklink") + ); + }, + async function test(dialogWin) { + let expectedFolder = "BookmarksToolbarFolderTitle"; + let expectedFolderName = PlacesUtils.getString(expectedFolder); + + let folderPicker = dialogWin.document.getElementById( + "editBMPanel_folderMenuList" + ); + + // Check the initial state of the folder picker. + await TestUtils.waitForCondition( + () => folderPicker.selectedItem.label == expectedFolderName, + "The folder is the expected one." + ); + } + ); + } +}); + +/** + * Verify that if we change the location, we persist that selection. + */ +add_task(async function test_change_location_panel() { + await clickBookmarkStar(win); + + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + + let { toolbarGuid, menuGuid, unfiledGuid } = PlacesUtils.bookmarks; + + let expectedFolderGuid = toolbarGuid; + + info("Pref value: " + Services.prefs.getCharPref(LOCATION_PREF, "")); + await TestUtils.waitForCondition( + () => menuList.getAttribute("selectedGuid") == expectedFolderGuid, + "Should initially select the unfiled or toolbar item" + ); + + // Now move this new bookmark to the menu: + let promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}, win); + await promisePopup; + + // Make sure we wait for the bookmark to be added. + let itemAddedPromise = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => url === TEST_URL) + ); + + // Wait for the pref to change + let prefChangedPromise = TestUtils.waitForPrefChange(LOCATION_PREF); + + // Click the choose item. + EventUtils.synthesizeMouseAtCenter( + win.document.getElementById("editBMPanel_bmRootItem"), + {}, + win + ); + + await TestUtils.waitForCondition( + () => menuList.getAttribute("selectedGuid") == menuGuid, + "Should select the menu folder item" + ); + + info("Waiting for transactions to finish."); + await Promise.all(win.gEditItemOverlay.transactionPromises); + info("Moved; waiting to hide panel."); + + await hideBookmarksPanel(win); + info("Waiting for pref change."); + await prefChangedPromise; + info("Waiting for item to be added."); + await itemAddedPromise; + + // Check that it's in the menu, and remove the bookmark: + let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL }); + is(bm?.parentGuid, menuGuid, "Bookmark was put in the menu."); + if (bm) { + await PlacesUtils.bookmarks.remove(bm); + } + + // Now create a new bookmark and check it starts in the menu + await clickBookmarkStar(win); + + let expectedFolder = "BookmarksMenuFolderTitle"; + Assert.equal( + menuList.label, + PlacesUtils.getString(expectedFolder), + `Should have menu folder selected by default` + ); + Assert.equal( + menuList.getAttribute("selectedGuid"), + menuGuid, + "Should have the correct default guid selected" + ); + + // Now select a different item. + promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}, win); + await promisePopup; + + // Click the toolbar item. + EventUtils.synthesizeMouseAtCenter( + win.document.getElementById("editBMPanel_toolbarFolderItem"), + {}, + win + ); + + await TestUtils.waitForCondition( + () => menuList.getAttribute("selectedGuid") == toolbarGuid, + "Should select the toolbar item" + ); + + await cancelBookmarkCreationInPanel(); + + is( + await PlacesUIUtils.defaultParentGuid, + menuGuid, + "Default folder should not change if we cancel the panel." + ); + + // Now open the panel for an existing bookmark whose parent doesn't match + // the default and check we don't overwrite the default folder. + let testBM = await PlacesUtils.bookmarks.insert({ + parentGuid: unfiledGuid, + title: "Yoink", + url: TEST_URL, + }); + await TestUtils.waitForCondition( + () => win.BookmarkingUI.star.hasAttribute("starred"), + "Wait for bookmark to show up for current page." + ); + await clickBookmarkStar(win); + + await hideBookmarksPanel(win); + is( + await PlacesUIUtils.defaultParentGuid, + menuGuid, + "Default folder should not change if we accept the panel, but didn't change folders." + ); + + await PlacesUtils.bookmarks.remove(testBM); +}); diff --git a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js new file mode 100644 index 0000000000..ea96282f30 --- /dev/null +++ b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js @@ -0,0 +1,255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URL = "http://www.mozilla.org"; +const TEST_TITLE = "example_title"; + +var gBookmarksToolbar = window.document.getElementById("PersonalToolbar"); +var dragDirections = { LEFT: 0, UP: 1, RIGHT: 2, DOWN: 3 }; + +/** + * Tests dragging on toolbar. + * + * We must test these 2 cases: + * - Dragging toward left, top, right should start a drag. + * - Dragging toward down should should open the container if the item is a + * container, drag the item otherwise. + * + * @param {object} aElement + * DOM node element we will drag + * @param {Array} aExpectedDragData + * Array of flavors and values in the form: + * [ ["text/plain: sometext", "text/html: <b>sometext</b>"], [...] ] + * Pass an empty array to check that drag even has been canceled. + * @param {number} aDirection + * Direction for the dragging gesture, see dragDirections helper object. + * @returns {Promise} Resolved once the drag gesture has been observed. + */ +function synthesizeDragWithDirection(aElement, aExpectedDragData, aDirection) { + let promise = new Promise(resolve => { + // Dragstart listener function. + gBookmarksToolbar.addEventListener("dragstart", function listener(event) { + info("A dragstart event has been trapped."); + var dataTransfer = event.dataTransfer; + is( + dataTransfer.mozItemCount, + aExpectedDragData.length, + "Number of dragged items should be the same." + ); + + for (var t = 0; t < dataTransfer.mozItemCount; t++) { + var types = dataTransfer.mozTypesAt(t); + var expecteditem = aExpectedDragData[t]; + is( + types.length, + expecteditem.length, + "Number of flavors for item " + t + " should be the same." + ); + + for (var f = 0; f < types.length; f++) { + is( + types[f], + expecteditem[f].substring(0, types[f].length), + "Flavor " + types[f] + " for item " + t + " should be the same." + ); + is( + dataTransfer.mozGetDataAt(types[f], t), + expecteditem[f].substring(types[f].length + 2), + "Contents for item " + + t + + " with flavor " + + types[f] + + " should be the same." + ); + } + } + + if (!aExpectedDragData.length) { + ok(event.defaultPrevented, "Drag has been canceled."); + } + + event.preventDefault(); + event.stopPropagation(); + + gBookmarksToolbar.removeEventListener("dragstart", listener); + + // This is likely to cause a click event, and, in case we are dragging a + // bookmark, an unwanted page visit. Prevent the click event. + aElement.addEventListener("click", prevent); + EventUtils.synthesizeMouse( + aElement, + startingPoint.x + xIncrement * 9, + startingPoint.y + yIncrement * 9, + { type: "mouseup" } + ); + aElement.removeEventListener("click", prevent); + + // Cleanup eventually opened menus. + if (aElement.localName == "menu" && aElement.open) { + aElement.open = false; + } + resolve(); + }); + }); + + var prevent = function (aEvent) { + aEvent.preventDefault(); + }; + + var xIncrement = 0; + var yIncrement = 0; + + switch (aDirection) { + case dragDirections.LEFT: + xIncrement = -1; + break; + case dragDirections.RIGHT: + xIncrement = +1; + break; + case dragDirections.UP: + yIncrement = -1; + break; + case dragDirections.DOWN: + yIncrement = +1; + break; + } + + var rect = aElement.getBoundingClientRect(); + var startingPoint = { + x: (rect.right - rect.left) / 2, + y: (rect.bottom - rect.top) / 2, + }; + + EventUtils.synthesizeMouse(aElement, startingPoint.x, startingPoint.y, { + type: "mousedown", + }); + EventUtils.synthesizeMouse( + aElement, + startingPoint.x + xIncrement * 1, + startingPoint.y + yIncrement * 1, + { type: "mousemove" } + ); + EventUtils.synthesizeMouse( + aElement, + startingPoint.x + xIncrement * 9, + startingPoint.y + yIncrement * 9, + { type: "mousemove" } + ); + + return promise; +} + +function getToolbarNodeForItemId(itemGuid) { + var children = document.getElementById("PlacesToolbarItems").children; + for (let child of children) { + if (itemGuid == child._placesNode.bookmarkGuid) { + return child; + } + } + return null; +} + +function getExpectedDataForPlacesNode(aNode) { + var wrappedNode = []; + var flavors = [ + "text/x-moz-place", + "text/x-moz-url", + "text/plain", + "text/html", + ]; + + flavors.forEach(function (aFlavor) { + var wrappedFlavor = aFlavor + ": " + PlacesUtils.wrapNode(aNode, aFlavor); + wrappedNode.push(wrappedFlavor); + }); + + return [wrappedNode]; +} + +add_setup(async function () { + var toolbar = document.getElementById("PersonalToolbar"); + var wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + await promiseSetToolbarVisibility(toolbar, false); + }); +}); + +add_task(async function test_drag_folder_on_toolbar() { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: TEST_TITLE, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + var element = getToolbarNodeForItemId(folder.guid); + isnot(element, null, "Found node on toolbar"); + + isnot( + element._placesNode, + null, + "Toolbar node has an associated Places node." + ); + var expectedData = getExpectedDataForPlacesNode(element._placesNode); + + info("Dragging left"); + await synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT); + + info("Dragging right"); + await synthesizeDragWithDirection( + element, + expectedData, + dragDirections.RIGHT + ); + + info("Dragging up"); + await synthesizeDragWithDirection(element, expectedData, dragDirections.UP); + + info("Dragging down"); + await synthesizeDragWithDirection(element, [], dragDirections.DOWN); + + await PlacesUtils.bookmarks.remove(folder); +}); + +add_task(async function test_drag_bookmark_on_toolbar() { + var bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: TEST_TITLE, + url: TEST_URL, + }); + + var element = getToolbarNodeForItemId(bookmark.guid); + isnot(element, null, "Found node on toolbar"); + + isnot( + element._placesNode, + null, + "Toolbar node has an associated Places node." + ); + var expectedData = getExpectedDataForPlacesNode(element._placesNode); + + info("Dragging left"); + await synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT); + + info("Dragging right"); + await synthesizeDragWithDirection( + element, + expectedData, + dragDirections.RIGHT + ); + + info("Dragging up"); + await synthesizeDragWithDirection(element, expectedData, dragDirections.UP); + + info("Dragging down"); + synthesizeDragWithDirection(element, expectedData, dragDirections.DOWN); + + await PlacesUtils.bookmarks.remove(bookmark); +}); diff --git a/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js b/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js new file mode 100644 index 0000000000..9f12e57ee9 --- /dev/null +++ b/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + // Clean before and after so we don't have anything in the folders. + await PlacesUtils.bookmarks.eraseEverything(); + + registerCleanupFunction(async function () { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +const TEST_FOLDER_NAME = "Test folder"; + +add_task(async function test_change_location_from_Toolbar() { + let newTabButton = document.getElementById("tabs-newtab-button"); + + let children = [ + { + title: "first", + url: "http://www.example.com/first", + }, + { + title: "second", + url: "http://www.example.com/second", + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + title: "third", + url: "http://www.example.com/third", + }, + ]; + let guid = PlacesUtils.history.makeGuid(); + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: TEST_FOLDER_NAME, + children, + }, + ], + }); + + let folder = getToolbarNodeForItemGuid(guid); + + let loadedPromises = children + .filter(item => "url" in item) + .map(item => + BrowserTestUtils.waitForNewTab(gBrowser, item.url, true, true) + ); + + let srcX = 10, + srcY = 10; + // We should drag upwards, since dragging downwards opens menu instead. + let stepX = 0, + stepY = -5; + + // We need to dispatch mousemove before dragging, to populate + // PlacesToolbar._cachedMouseMoveEvent, with the cursor position after the + // first step, so that the places code detects it as dragging upward. + EventUtils.synthesizeMouse(folder, srcX + stepX, srcY + stepY, { + type: "mousemove", + }); + + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: folder, + destElement: newTabButton, + srcX, + srcY, + stepX, + stepY, + }); + + let tabs = await Promise.all(loadedPromises); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + ok(true); +}); diff --git a/browser/components/places/tests/browser/browser_editBookmark_keywords.js b/browser/components/places/tests/browser/browser_editBookmark_keywords.js new file mode 100644 index 0000000000..5489a06165 --- /dev/null +++ b/browser/components/places/tests/browser/browser_editBookmark_keywords.js @@ -0,0 +1,64 @@ +"use strict"; + +const TEST_URL = "about:blank"; + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_URL, + waitForStateStop: true, + }); + + let library = await promiseLibrary("UnfiledBookmarks"); + registerCleanupFunction(async () => { + await promiseLibraryClosed(library); + await PlacesUtils.bookmarks.eraseEverything(); + await BrowserTestUtils.removeTab(tab); + }); + + let keywordField = library.document.getElementById( + "editBMPanel_keywordField" + ); + + for (let i = 0; i < 2; ++i) { + let bm = await PlacesUtils.bookmarks.insert({ + url: `http://www.test${i}.me/`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + let node = library.ContentTree.view.view.nodeForTreeIndex(i); + is(node.bookmarkGuid, bm.guid, "Found the expected bookmark"); + // Select the bookmark. + library.ContentTree.view.selectNode(node); + synthesizeClickOnSelectedTreeCell(library.ContentTree.view); + + is( + library.document.getElementById("editBMPanel_keywordField").value, + "", + "The keyword field should be empty" + ); + info("Add a keyword to the bookmark"); + const promise = PlacesTestUtils.waitForNotification( + "bookmark-keyword-changed" + ); + keywordField.focus(); + keywordField.value = "kw"; + EventUtils.sendString(i.toString(), library); + keywordField.blur(); + const events = await promise; + is(events.length, 1, "Number of events fired is correct"); + const keyword = events[0].keyword; + is(keyword, `kw${i}`, "The new keyword value is correct"); + } + + for (let i = 0; i < 2; ++i) { + let entry = await PlacesUtils.keywords.fetch({ + url: `http://www.test${i}.me/`, + }); + is( + entry.keyword, + `kw${i}`, + `The keyword for http://www.test${i}.me/ is correct` + ); + } +}); diff --git a/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js new file mode 100644 index 0000000000..8d4d650984 --- /dev/null +++ b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the Bookmarks Toolbar and Sidebar can be enabled from the Bookmarks Menu ("View history, + * saved bookmarks, and more" button. + */ + +// Cleanup. +registerCleanupFunction(async () => { + CustomizableUI.setToolbarVisibility("PersonalToolbar", false); + CustomizableUI.removeWidgetFromArea("library-button"); + SidebarUI.hide(); +}); + +async function selectAppMenuView(buttonId, viewId) { + let btn; + await TestUtils.waitForCondition(() => { + btn = document.getElementById(buttonId); + return btn; + }, "Should have the " + buttonId + " button"); + btn.click(); + let view = document.getElementById(viewId); + let viewPromise = BrowserTestUtils.waitForEvent(view, "ViewShown"); + await viewPromise; +} + +async function openBookmarkingPanelInLibraryToolbarButton() { + await selectAppMenuView("library-button", "appMenu-libraryView"); + await selectAppMenuView( + "appMenu-library-bookmarks-button", + "PanelUI-bookmarks" + ); +} + +add_task(async function test_enable_toolbar() { + CustomizableUI.addWidgetToArea("library-button", "nav-bar"); + + await openBookmarkingPanelInLibraryToolbarButton(); + let toolbar = document.getElementById("PersonalToolbar"); + Assert.ok(toolbar.collapsed, "Bookmarks Toolbar is hidden"); + + let viewBookmarksToolbarBtn; + await TestUtils.waitForCondition(() => { + viewBookmarksToolbarBtn = document.getElementById( + "panelMenu_viewBookmarksToolbar" + ); + return viewBookmarksToolbarBtn; + }, "Should have the library 'View Bookmarks Toolbar' button."); + viewBookmarksToolbarBtn.click(); + await TestUtils.waitForCondition( + () => !toolbar.collapsed, + "Should have the Bookmarks Toolbar enabled." + ); + Assert.ok(!toolbar.collapsed, "Bookmarks Toolbar is enabled"); +}); diff --git a/browser/components/places/tests/browser/browser_forgetthissite.js b/browser/components/places/tests/browser/browser_forgetthissite.js new file mode 100644 index 0000000000..380497aed7 --- /dev/null +++ b/browser/components/places/tests/browser/browser_forgetthissite.js @@ -0,0 +1,262 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Tests the "Forget About This Site" button from the libary view +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +const TEST_URIs = [ + { title: "0", uri: "http://example.com" }, + { title: "1", uri: "http://www.mozilla.org/test1" }, + { title: "2", uri: "http://www.mozilla.org/test2" }, + { title: "3", uri: "https://192.168.200.1/login.html" }, +]; + +async function setup() { + registerCleanupFunction(async function () { + // Clean up any leftover stubs. + sinon.restore(); + }); + + let places = []; + let transition = PlacesUtils.history.TRANSITION_TYPED; + TEST_URIs.forEach(({ title, uri }) => + places.push({ uri: Services.io.newURI(uri), transition, title }) + ); + await PlacesTestUtils.addVisits(places); +} + +async function teardown(organizer) { + // Close the library window. + await promiseLibraryClosed(organizer); + await PlacesUtils.history.clear(); +} + +// Selects the sites specified by sitesToSelect +// If multiple sites are selected they can't be forgotten +// Should forget selects the answer in the confirmation dialogue +// removedEntries specifies which entries should be forgotten +async function testForgetAboutThisSite( + sitesToSelect, + shouldForget, + removedEntries, + cancelConfirmWithEsc = false +) { + if (cancelConfirmWithEsc) { + ok( + !shouldForget, + "If cancelConfirmWithEsc is set we don't expect to clear entries." + ); + } + + ok(PlacesUtils, "checking PlacesUtils, running in chrome context?"); + await setup(); + let organizer = await promiseHistoryView(); + let doc = organizer.document; + let tree = doc.getElementById("placeContent"); + + //Sort by name in descreasing order + tree.view._result.sortingMode = + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING; + + let selection = tree.view.selection; + selection.clearSelection(); + sitesToSelect.forEach(index => selection.rangedSelect(index, index, true)); + + let selectionCount = sitesToSelect.length; + is( + selection.count, + selectionCount, + "The selected range is as big as expected" + ); + // Open the context menu. + let contextmenu = doc.getElementById("placesContext"); + let popupShown = promisePopupShown(contextmenu); + + // Get cell coordinates. + let rect = tree.getCoordsForCellItem( + sitesToSelect[0], + tree.columns[0], + "text" + ); + // Initiate a context menu for the selected cell. + EventUtils.synthesizeMouse( + tree.body, + rect.x + rect.width / 2, + rect.y + rect.height / 2, + { type: "contextmenu", button: 2 }, + organizer + ); + await popupShown; + + let forgetThisSite = doc.getElementById("placesContext_deleteHost"); + let hideForgetThisSite = selectionCount > 1; + is( + forgetThisSite.hidden, + hideForgetThisSite, + `The Forget this site menu item should ${ + hideForgetThisSite ? "" : "not " + }be hidden with ${selectionCount} items selected` + ); + if (hideForgetThisSite) { + // Close the context menu. + contextmenu.hidePopup(); + await teardown(organizer); + return; + } + + // Resolves once the confirmation prompt has been closed. + let promptPromise; + + // Cancel prompt via esc key. We have to get the prompt closed promise + // ourselves. + if (cancelConfirmWithEsc) { + promptPromise = PromptTestUtils.waitForPrompt(organizer, { + modalType: Services.prompt.MODAL_TYPE_WINDOW, + promptType: "confirmEx", + }).then(dialog => { + let dialogWindow = dialog.ui.prompt; + let dialogClosedPromise = BrowserTestUtils.waitForEvent( + dialogWindow.opener, + "DOMModalDialogClosed" + ); + EventUtils.synthesizeKey("KEY_Escape", undefined, dialogWindow); + + return dialogClosedPromise; + }); + } else { + // Close prompt via buttons. PromptTestUtils supplies the closed promise. + promptPromise = PromptTestUtils.handleNextPrompt( + organizer, + { modalType: Services.prompt.MODAL_TYPE_WINDOW, promptType: "confirmEx" }, + { buttonNumClick: shouldForget ? 0 : 1 } + ); + } + + // If we cancel the prompt, create stubs to check that none of the clear + // methods are called. + if (!shouldForget) { + sinon.stub(ForgetAboutSite, "removeDataFromBaseDomain").resolves(); + sinon.stub(ForgetAboutSite, "removeDataFromDomain").resolves(); + } + + let pageRemovedEventPromise; + if (shouldForget) { + pageRemovedEventPromise = + PlacesTestUtils.waitForNotification("page-removed"); + } + + // Execute the delete command. + contextmenu.activateItem(forgetThisSite); + + // Wait for prompt to be handled. + await promptPromise; + + // If we expect to remove items, wait the page-removed event to fire. If we + // don't wait, we may test the list before any items have been removed. + await pageRemovedEventPromise; + + if (!shouldForget) { + ok( + ForgetAboutSite.removeDataFromBaseDomain.notCalled && + ForgetAboutSite.removeDataFromDomain.notCalled, + "Should not call ForgetAboutSite when the confirmation prompt is cancelled." + ); + // Remove the stubs. + sinon.restore(); + } + + // Check that the entries have been removed. + await Promise.all( + removedEntries.map(async ({ uri }) => { + Assert.ok( + !(await PlacesUtils.history.fetch(uri)), + `History entry for ${uri} has been correctly removed` + ); + }) + ); + await Promise.all( + TEST_URIs.filter(x => !removedEntries.includes(x)).map(async ({ uri }) => { + Assert.ok( + await PlacesUtils.history.fetch(uri), + `History entry for ${uri} has been kept` + ); + }) + ); + + // Cleanup. + await teardown(organizer); +} + +/* + * Opens the history view in the PlacesOrganziner window + * @returns {Promise} + * @resolves The PlacesOrganizer + */ +async function promiseHistoryView() { + let organizer = await promiseLibrary(); + + // Select History in the left pane. + let po = organizer.PlacesOrganizer; + po.selectLeftPaneBuiltIn("History"); + + let histContainer = po._places.selectedNode.QueryInterface( + Ci.nsINavHistoryContainerResultNode + ); + histContainer.containerOpen = true; + po._places.selectNode(histContainer.getChild(0)); + + return organizer; +} +/* + * @returns {Promise} + * @resolves once the popup is shown + */ +function promisePopupShown(popup) { + return new Promise(resolve => { + popup.addEventListener( + "popupshown", + function () { + resolve(); + }, + { capture: true, once: true } + ); + }); +} + +// This test makes sure that the Forget This Site command is hidden for multiple +// selections. +add_task(async function selectMultiple() { + await testForgetAboutThisSite([0, 1]); +}); + +// This test makes sure that forgetting "http://www.mozilla.org/test2" also removes "http://www.mozilla.org/test1" +add_task(async function forgettingBasedomain() { + await testForgetAboutThisSite([1], true, TEST_URIs.slice(1, 3)); +}); + +// This test makes sure that forgetting by IP address works +add_task(async function forgettingIPAddress() { + await testForgetAboutThisSite([3], true, TEST_URIs.slice(3, 4)); +}); + +// This test makes sure that forgetting file URLs works +add_task(async function dontAlwaysForget() { + await testForgetAboutThisSite([0], false, []); +}); + +// When cancelling the confirmation prompt via ESC key, no entries should be +// cleared. +add_task(async function cancelConfirmWithEsc() { + await testForgetAboutThisSite([0], false, [], true); +}); diff --git a/browser/components/places/tests/browser/browser_history_sidebar_search.js b/browser/components/places/tests/browser/browser_history_sidebar_search.js new file mode 100644 index 0000000000..44d51eec31 --- /dev/null +++ b/browser/components/places/tests/browser/browser_history_sidebar_search.js @@ -0,0 +1,71 @@ +add_task(async function test() { + let sidebar = document.getElementById("sidebar"); + + // Visited pages listed by descending visit date. + let pages = [ + "http://sidebar.mozilla.org/a", + "http://sidebar.mozilla.org/b", + "http://sidebar.mozilla.org/c", + "http://www.mozilla.org/d", + ]; + + // Number of pages that will be filtered out by the search. + const FILTERED_COUNT = 1; + + await PlacesUtils.history.clear(); + + // Add some visited page. + let time = Date.now(); + let places = []; + for (let i = 0; i < pages.length; i++) { + places.push({ + uri: NetUtil.newURI(pages[i]), + visitDate: (time - i) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(places); + + await withSidebarTree("history", function () { + info("Set 'by last visited' view"); + sidebar.contentDocument.getElementById("bylastvisited").doCommand(); + let tree = sidebar.contentDocument.getElementById("historyTree"); + check_tree_order(tree, pages); + + // Set a search value. + let searchBox = sidebar.contentDocument.getElementById("search-box"); + ok(searchBox, "search box is in context"); + searchBox.value = "sidebar.mozilla"; + searchBox.doCommand(); + check_tree_order(tree, pages, -FILTERED_COUNT); + + info("Reset the search"); + searchBox.value = ""; + searchBox.doCommand(); + check_tree_order(tree, pages); + }); + + await PlacesUtils.history.clear(); +}); + +function check_tree_order(tree, pages, aNumberOfRowsDelta = 0) { + let treeView = tree.view; + let columns = tree.columns; + is(columns.count, 1, "There should be only 1 column in the sidebar"); + + let found = 0; + for (let i = 0; i < treeView.rowCount; i++) { + let node = treeView.nodeForTreeIndex(i); + // We could inherit delayed visits from previous tests, skip them. + if (!pages.includes(node.uri)) { + continue; + } + is( + node.uri, + pages[i], + "Node is in correct position based on its visit date" + ); + found++; + } + is(found, pages.length + aNumberOfRowsDelta, "Found all expected results"); +} diff --git a/browser/components/places/tests/browser/browser_import_button.js b/browser/components/places/tests/browser/browser_import_button.js new file mode 100644 index 0000000000..146fd746f8 --- /dev/null +++ b/browser/components/places/tests/browser/browser_import_button.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kPref = "browser.bookmarks.addedImportButton"; + +/** + * Verify that we add the import button only if there aren't enough bookmarks + * in the toolbar. + */ +add_task(async function test_bookmark_import_button() { + let bookmarkCount = ( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid) + ).childCount; + Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks"); + + ok( + !document.getElementById("import-button"), + "Shouldn't have button to start with." + ); + await PlacesUIUtils.maybeAddImportButton(); + ok(document.getElementById("import-button"), "Button should be added."); + is(Services.prefs.getBoolPref(kPref), true, "Pref should be set."); + + CustomizableUI.reset(); + + // Add some bookmarks. This should stop the import button from being inserted. + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + let bookmarks = await Promise.all( + ["firefox", "rules", "yo"].map(n => + PlacesUtils.bookmarks.insert({ + parentGuid, + url: `https://example.com/${n}`, + title: n.toString(), + }) + ) + ); + + // Ensure we remove items after this task, or worst-case after this test + // file has completed. + let removeAllBookmarks = () => { + let removals = bookmarks.map(b => PlacesUtils.bookmarks.remove(b.guid)); + bookmarks = []; + return Promise.all(removals); + }; + registerCleanupFunction(removeAllBookmarks); + + await PlacesUIUtils.maybeAddImportButton(); + ok( + !document.getElementById("import-button"), + "Button should not be added if we have bookmarks." + ); + + // Just in case, for future tests we run: + CustomizableUI.reset(); + + await removeAllBookmarks(); +}); + +/** + * Verify the button gets removed when we import bookmarks successfully. + */ +add_task(async function test_bookmark_import_button_removal() { + let bookmarkCount = ( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid) + ).childCount; + Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks"); + + ok( + !document.getElementById("import-button"), + "Shouldn't have button to start with." + ); + await PlacesUIUtils.maybeAddImportButton(); + ok(document.getElementById("import-button"), "Button should be added."); + is(Services.prefs.getBoolPref(kPref), true, "Pref should be set."); + + PlacesUIUtils.removeImportButtonWhenImportSucceeds(); + + Services.obs.notifyObservers( + null, + "Migration:ItemAfterMigrate", + MigrationUtils.resourceTypes.BOOKMARKS + ); + + is( + Services.prefs.getBoolPref(kPref, false), + true, + "Pref should stay without import." + ); + ok(document.getElementById("import-button"), "Button should still be there."); + + // OK, actually add some bookmarks: + MigrationUtils._importQuantities.bookmarks = 5; + Services.obs.notifyObservers( + null, + "Migration:ItemAfterMigrate", + MigrationUtils.resourceTypes.BOOKMARKS + ); + + is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed."); + ok( + !document.getElementById("import-button"), + "Button should have been removed." + ); + + // Reset this, otherwise subsequent tests are going to have a bad time. + MigrationUtils._importQuantities.bookmarks = 0; +}); + +/** + * Check that if the user removes the button, the next startup + * we clear the pref and stop monitoring to remove the item. + */ +add_task(async function test_bookmark_import_button_removal_cleanup() { + let bookmarkCount = ( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid) + ).childCount; + Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks"); + + ok( + !document.getElementById("import-button"), + "Shouldn't have button to start with." + ); + await PlacesUIUtils.maybeAddImportButton(); + ok(document.getElementById("import-button"), "Button should be added."); + is(Services.prefs.getBoolPref(kPref), true, "Pref should be set."); + + // Simulate the user removing the item. + CustomizableUI.removeWidgetFromArea("import-button"); + + // We'll call this next startup: + PlacesUIUtils.removeImportButtonWhenImportSucceeds(); + + // And it should clean up the pref: + is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed."); +}); + +/** + * Check that if migration (silently) errors, we still remove the button + * _if_ we imported any bookmarks. + */ +add_task(async function test_bookmark_import_button_errors() { + let bookmarkCount = ( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid) + ).childCount; + Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks"); + + ok( + !document.getElementById("import-button"), + "Shouldn't have button to start with." + ); + await PlacesUIUtils.maybeAddImportButton(); + ok(document.getElementById("import-button"), "Button should be added."); + is(Services.prefs.getBoolPref(kPref), true, "Pref should be set."); + + PlacesUIUtils.removeImportButtonWhenImportSucceeds(); + + Services.obs.notifyObservers( + null, + "Migration:ItemError", + MigrationUtils.resourceTypes.BOOKMARKS + ); + + is( + Services.prefs.getBoolPref(kPref, false), + true, + "Pref should stay when fatal error happens." + ); + ok(document.getElementById("import-button"), "Button should still be there."); + + // OK, actually add some bookmarks: + MigrationUtils._importQuantities.bookmarks = 5; + Services.obs.notifyObservers( + null, + "Migration:ItemError", + MigrationUtils.resourceTypes.BOOKMARKS + ); + + is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed."); + ok( + !document.getElementById("import-button"), + "Button should have been removed." + ); + + MigrationUtils._importQuantities.bookmarks = 0; +}); diff --git a/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js b/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js new file mode 100644 index 0000000000..fab8b4c3b0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Test whether or not that Most Recent Visit of bookmark in library window will + * update properly when removing the history. + */ + +const TEST_URLS = ["https://example.com/", "https://example.org/"]; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[0], + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[1], + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URLS[1], + }); + PlacesUtils.tagging.tagURI(makeURI(TEST_URLS[0]), ["test"]); + PlacesUtils.tagging.tagURI(makeURI(TEST_URLS[1]), ["test"]); + + registerCleanupFunction(async () => { + PlacesUtils.tagging.untagURI(makeURI(TEST_URLS[0]), ["test"]); + PlacesUtils.tagging.untagURI(makeURI(TEST_URLS[1]), ["test"]); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function folder() { + info("Open bookmarked urls to update most recent visit time"); + await updateMostRecentVisitTime(); + + info("Open library for bookmarks toolbar"); + const library = await promiseLibrary("BookmarksToolbar"); + + info("Add Most Recent Visit column"); + await showLibraryColumn(library, "placesContentDate"); + + info("Check the initial content"); + const tree = library.document.getElementById("placeContent"); + Assert.equal(tree.view.rowCount, 3, "All bookmarks are shown"); + assertRow(tree, 0, TEST_URLS[0], true); + assertRow(tree, 1, TEST_URLS[1], true); + assertRow(tree, 2, TEST_URLS[1], true); + + info("Clear all visits data"); + await PlacesUtils.history.remove(TEST_URLS); + + info("Check whether or not the content are updated"); + assertRow(tree, 0, TEST_URLS[0], false); + assertRow(tree, 1, TEST_URLS[1], false); + assertRow(tree, 2, TEST_URLS[1], false); + + info("Close library window"); + await promiseLibraryClosed(library); +}); + +add_task(async function tags() { + info("Open bookmarked urls to update most recent visit time"); + await updateMostRecentVisitTime(); + + info("Open library for bookmarks toolbar"); + const library = await promiseLibrary("BookmarksToolbar"); + + info("Add Most Recent Visit column"); + await showLibraryColumn(library, "placesContentDate"); + + info("Open test tag"); + const PO = library.PlacesOrganizer; + PO.selectLeftPaneBuiltIn("Tags"); + const tagsNode = PO._places.selectedNode; + PlacesUtils.asContainer(tagsNode).containerOpen = true; + const tag = tagsNode.getChild(0); + PO._places.selectNode(tag); + + info("Check the initial content"); + const tree = library.ContentTree.view; + Assert.equal(tree.view.rowCount, 3, "All bookmarks are shown"); + assertRow(tree, 0, TEST_URLS[0], true); + assertRow(tree, 1, TEST_URLS[1], true); + assertRow(tree, 2, TEST_URLS[1], true); + + info("Clear all visits data"); + await PlacesUtils.history.remove(TEST_URLS); + + info("Check whether or not the content are updated"); + assertRow(tree, 0, TEST_URLS[0], false); + assertRow(tree, 1, TEST_URLS[1], false); + assertRow(tree, 2, TEST_URLS[1], false); + + info("Close library window"); + await promiseLibraryClosed(library); +}); + +async function updateMostRecentVisitTime() { + for (const url of TEST_URLS) { + const onLoaded = BrowserTestUtils.browserLoaded(gBrowser, false, url); + BrowserTestUtils.startLoadingURIString(gBrowser, url); + await onLoaded; + } +} + +function assertRow(tree, targeRow, expectedUrl, expectMostRecentVisitHasValue) { + const url = tree.view.getCellText(targeRow, tree.columns.placesContentUrl); + Assert.equal(url, expectedUrl, "URL is correct"); + const mostRecentVisit = tree.view.getCellText( + targeRow, + tree.columns.placesContentDate + ); + Assert.equal( + !!mostRecentVisit, + expectMostRecentVisitHasValue, + "Most Recent Visit data is in the cell correctly" + ); +} diff --git a/browser/components/places/tests/browser/browser_library_bookmark_pages.js b/browser/components/places/tests/browser/browser_library_bookmark_pages.js new file mode 100644 index 0000000000..474adb956d --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_bookmark_pages.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the a new bookmark is correctly selected after being created via + * the bookmark dialog. + */ +"use strict"; + +const TEST_URIS = ["https://example1.com/", "https://example2.com/"]; +let library; + +add_setup(async function () { + await PlacesTestUtils.addVisits(TEST_URIS); + + library = await promiseLibrary("History"); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_bookmark_page() { + library.ContentTree.view.selectPlaceURI(TEST_URIS[0]); + + await withBookmarksDialog( + true, + async () => { + // Open the context menu. + let placesContext = library.document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + await promisePopup; + let properties = library.document.getElementById( + "placesContext_createBookmark" + ); + placesContext.activateItem(properties); + }, + async dialogWin => { + Assert.strictEqual( + dialogWin.BookmarkPropertiesPanel._itemType, + 0, + "Should have loaded a bookmark dialog" + ); + Assert.equal( + dialogWin.document.getElementById("editBMPanel_locationField").value, + TEST_URIS[0], + "Should have opened the dialog with the correct uri to be bookmarked" + ); + } + ); +}); + +add_task(async function test_bookmark_pages() { + library.ContentTree.view.selectAll(); + + await withBookmarksDialog( + true, + async () => { + // Open the context menu. + let placesContext = library.document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + await promisePopup; + let properties = library.document.getElementById( + "placesContext_createBookmark" + ); + placesContext.activateItem(properties); + }, + async dialogWin => { + Assert.strictEqual( + dialogWin.BookmarkPropertiesPanel._itemType, + 1, + "Should have loaded a create bookmark folder dialog" + ); + Assert.deepEqual( + dialogWin.BookmarkPropertiesPanel._URIs.map(uri => uri.uri.spec), + // The list here is reversed, because that's the order they're shown + // in the view. + [TEST_URIS[1], TEST_URIS[0]], + "Should have got the correct URIs for adding to the folder" + ); + } + ); +}); diff --git a/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js b/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js new file mode 100644 index 0000000000..23d3b30564 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that a tag can be added to multiple bookmarks at once from the library. + */ +"use strict"; + +const TEST_URLS = ["about:buildconfig", "about:robots"]; + +add_task(async function test_bulk_tag_from_library() { + // Create multiple bookmarks. + for (const url of TEST_URLS) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + }); + } + + // Open library panel. + const library = await promiseLibrary("UnfiledBookmarks"); + const cleanupFn = async () => { + await promiseLibraryClosed(library); + await PlacesUtils.bookmarks.eraseEverything(); + }; + registerCleanupFunction(cleanupFn); + + // Add a tag to multiple bookmarks. + library.ContentTree.view.selectAll(); + const promiseAllTagsChanged = TEST_URLS.map(url => + PlacesTestUtils.waitForNotification("bookmark-tags-changed", events => + events.some(evt => evt.url === url) + ) + ); + const tag = "some, tag"; + const tagWithDuplicates = `${tag}, tag`; + fillBookmarkTextField("editBMPanel_tagsField", tagWithDuplicates, library); + await Promise.all(promiseAllTagsChanged); + await TestUtils.waitForCondition( + () => + library.document.getElementById("editBMPanel_tagsField").value === tag, + "Input field matches the new tags and duplicates are removed." + ); + + // Verify that the bookmarks were tagged successfully. + for (const url of TEST_URLS) { + Assert.deepEqual( + PlacesUtils.tagging.getTagsForURI(Services.io.newURI(url)), + ["some", "tag"], + url + " should have the correct tags." + ); + } + await cleanupFn(); +}); + +add_task(async function test_bulk_tag_tags_selector() { + // Create multiple bookmarks with a common tag. + for (const [i, url] of TEST_URLS.entries()) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), [ + "common", + `unique_${i}`, + ]); + } + + // Open library panel. + const library = await promiseLibrary("UnfiledBookmarks"); + const cleanupFn = async () => { + await promiseLibraryClosed(library); + await PlacesUtils.bookmarks.eraseEverything(); + }; + registerCleanupFunction(cleanupFn); + + // Open tags selector. + library.document.getElementById("editBMPanel_tagsSelectorRow").hidden = false; + + // Select all bookmarks. + const tagsSelector = library.document.getElementById( + "editBMPanel_tagsSelector" + ); + library.ContentTree.view.selectAll(); + + // Verify that the input field only shows the common tag. + await TestUtils.waitForCondition( + () => + library.document.getElementById("editBMPanel_tagsField").value === + "common", + "Input field only shows the common tag." + ); + + // Verify that the tags selector shows all tags, and only the common one is + // checked. + async function checkTagsSelector(aAvailableTags, aCheckedTags) { + let tags = await PlacesUtils.bookmarks.fetchTags(); + is(tags.length, aAvailableTags.length, "Check tags list"); + let children = tagsSelector.children; + is( + children.length, + aAvailableTags.length, + "Found expected number of tags in the tags selector" + ); + + Array.prototype.forEach.call(children, function (aChild) { + let tag = aChild.querySelector("label").getAttribute("value"); + ok(true, "Found tag '" + tag + "' in the selector"); + ok(aAvailableTags.includes(tag), "Found expected tag"); + let checked = aChild.getAttribute("checked") == "true"; + is(checked, aCheckedTags.includes(tag), "Tag is correctly marked"); + }); + } + + async function promiseTagSelectorUpdated(task) { + let promise = BrowserTestUtils.waitForEvent( + tagsSelector, + "BookmarkTagsSelectorUpdated" + ); + + await task(); + return promise; + } + + info("Check the initial common tag."); + await checkTagsSelector(["common", "unique_0", "unique_1"], ["common"]); + + // Verify that the common tag can be edited. + await promiseTagSelectorUpdated(() => { + info("Edit the common tag."); + fillBookmarkTextField("editBMPanel_tagsField", "common_updated", library); + }); + await checkTagsSelector( + ["common_updated", "unique_0", "unique_1"], + ["common_updated"] + ); + + // Verify that the common tag can be removed. + await promiseTagSelectorUpdated(() => { + info("Remove the commmon tag."); + fillBookmarkTextField("editBMPanel_tagsField", "", library); + }); + await checkTagsSelector(["unique_0", "unique_1"], []); + + await cleanupFn(); +}); diff --git a/browser/components/places/tests/browser/browser_library_commands.js b/browser/components/places/tests/browser/browser_library_commands.js new file mode 100644 index 0000000000..254c9503c2 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_commands.js @@ -0,0 +1,335 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test enabled commands in the left pane folder of the Library. + */ + +const TEST_URI = NetUtil.newURI("http://www.mozilla.org/"); + +registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_date_container() { + let library = await promiseLibrary(); + info("Ensure date containers under History cannot be cut but can be deleted"); + + await PlacesTestUtils.addVisits(TEST_URI); + + // Select and open the left pane "History" query. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("History"); + Assert.notEqual( + PO._places.selectedNode, + null, + "We correctly selected History" + ); + + // Check that both delete and cut commands are disabled, cause this is + // a child of the left pane folder. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is disabled" + ); + let historyNode = PlacesUtils.asContainer(PO._places.selectedNode); + historyNode.containerOpen = true; + + // Check that we have a child container. It is "Today" container. + Assert.equal(historyNode.childCount, 1, "History node has one child"); + let todayNode = historyNode.getChild(0); + let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0"); + Assert.equal( + todayNode.title, + todayNodeExpectedTitle, + "History child is the expected container" + ); + + // Select "Today" container. + PO._places.selectNode(todayNode); + Assert.equal( + PO._places.selectedNode, + todayNode, + "We correctly selected Today container" + ); + // Check that delete command is enabled but cut command is disabled, cause + // this is an history item. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + + // Execute the delete command and check visit has been removed. + const promiseURIRemoved = PlacesTestUtils.waitForNotification( + "page-removed", + events => events[0].url === TEST_URI.spec + ); + PO._places.controller.doCommand("cmd_delete"); + const removeEvents = await promiseURIRemoved; + Assert.ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + + // Test live update of "History" query. + Assert.equal(historyNode.childCount, 0, "History node has no more children"); + + historyNode.containerOpen = false; + + Assert.ok( + !(await PlacesUtils.history.hasVisits(TEST_URI)), + "Visit has been removed" + ); + + library.close(); +}); + +add_task(async function test_query_on_toolbar() { + let library = await promiseLibrary(); + info("Ensure queries can be cut or deleted"); + + // Select and open the left pane "Bookmarks Toolbar" folder. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("BookmarksToolbar"); + Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection"); + Assert.equal( + PlacesUtils.getConcreteItemGuid(PO._places.selectedNode), + PlacesUtils.bookmarks.toolbarGuid, + "We have correctly selected bookmarks toolbar node." + ); + + // Check that both cut and delete commands are disabled, cause this is a child + // of the All Bookmarks special query. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is disabled" + ); + + let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode); + toolbarNode.containerOpen = true; + + // Add an History query to the toolbar. + let query = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "place:sort=4", + title: "special_query", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + + // Get first child and check it is the just inserted query. + Assert.greater(toolbarNode.childCount, 0, "Toolbar node has children"); + let queryNode = toolbarNode.getChild(0); + Assert.equal( + queryNode.title, + "special_query", + "Query node is correctly selected" + ); + + // Select query node. + PO._places.selectNode(queryNode); + Assert.equal( + PO._places.selectedNode, + queryNode, + "We correctly selected query node" + ); + + // Check that both cut and delete commands are enabled. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is enabled" + ); + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + + // Execute the delete command and check bookmark has been removed. + let promiseItemRemoved = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => events.some(event => query.guid == event.guid) + ); + PO._places.controller.doCommand("cmd_delete"); + await promiseItemRemoved; + + Assert.equal( + await PlacesUtils.bookmarks.fetch(query.guid), + null, + "Query node bookmark has been correctly removed" + ); + + toolbarNode.containerOpen = false; + + library.close(); +}); + +add_task(async function test_search_contents() { + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "example page", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + + let library = await promiseLibrary(); + info("Ensure query contents can be cut or deleted"); + + // Select and open the left pane "Bookmarks Toolbar" folder. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("BookmarksToolbar"); + Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection"); + Assert.equal( + PlacesUtils.getConcreteItemGuid(PO._places.selectedNode), + PlacesUtils.bookmarks.toolbarGuid, + "We have correctly selected bookmarks toolbar node." + ); + + let searchBox = library.document.getElementById("searchFilter"); + searchBox.value = "example"; + library.PlacesSearchBox.search(searchBox.value); + + let bookmarkNode = library.ContentTree.view.selectedNode; + Assert.equal( + bookmarkNode.uri, + "http://example.com/", + "Found the expected bookmark" + ); + + // Check that both cut and delete commands are enabled. + Assert.ok( + library.ContentTree.view.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + library.ContentTree.view.controller.isCommandEnabled("cmd_cut"), + "Cut command is enabled" + ); + Assert.ok( + library.ContentTree.view.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + + library.close(); +}); + +add_task(async function test_tags() { + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "example page", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]); + + let library = await promiseLibrary(); + info("Ensure query contents can be cut or deleted"); + + // Select and open the left pane "Bookmarks Toolbar" folder. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("Tags"); + let tagsNode = PO._places.selectedNode; + Assert.notEqual(tagsNode, null, "We have a valid selection"); + let tagsTitle = PlacesUtils.getString("TagsFolderTitle"); + Assert.equal(tagsNode.title, tagsTitle, "Tags has been properly selected"); + + // Check that both cut and delete commands are disabled. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is disabled" + ); + + // Now select the tag. + PlacesUtils.asContainer(tagsNode).containerOpen = true; + let tag = tagsNode.getChild(0); + PO._places.selectNode(tag); + Assert.equal( + PO._places.selectedNode.title, + "test", + "The created tag has been properly selected" + ); + + // Check that cut is disabled but delete is enabled. + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !PO._places.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + + let bookmarkNode = library.ContentTree.view.selectedNode; + Assert.equal( + bookmarkNode.uri, + "http://example.com/", + "Found the expected bookmark" + ); + + // Check that both cut and delete commands are enabled. + Assert.ok( + library.ContentTree.view.controller.isCommandEnabled("cmd_copy"), + "Copy command is enabled" + ); + Assert.ok( + !library.ContentTree.view.controller.isCommandEnabled("cmd_cut"), + "Cut command is disabled" + ); + Assert.ok( + library.ContentTree.view.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + + tagsNode.containerOpen = false; + + library.close(); +}); diff --git a/browser/components/places/tests/browser/browser_library_delete.js b/browser/components/places/tests/browser/browser_library_delete.js new file mode 100644 index 0000000000..fe95be0604 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_delete.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Library correctly handles deletes. + */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const TEST_URL = "http://www.batch.delete.me/"; + +var gLibrary; + +add_task(async function test_setup() { + gLibrary = await promiseLibrary(); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + // Close Library window. + gLibrary.close(); + }); +}); + +add_task(async function test_create_and_remove_bookmarks() { + let bmChildren = []; + for (let i = 0; i < 10; i++) { + bmChildren.push({ + title: `bm${i}`, + url: TEST_URL, + }); + } + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "deleteme", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: bmChildren, + }, + { + title: "keepme", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + + // Select and open the left pane "History" query. + let PO = gLibrary.PlacesOrganizer; + PO.selectLeftPaneBuiltIn("UnfiledBookmarks"); + Assert.notEqual(PO._places.selectedNode, null, "Selected unsorted bookmarks"); + + let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode); + Assert.equal(unsortedNode.childCount, 2, "Unsorted node has 2 children"); + let folderNode = unsortedNode.getChild(0); + Assert.equal( + folderNode.title, + "deleteme", + "Folder found in unsorted bookmarks" + ); + + // Check delete command is available. + PO._places.selectNode(folderNode); + Assert.equal( + PO._places.selectedNode.title, + "deleteme", + "Folder node selected" + ); + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + let promiseItemRemovedNotification = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => events.some(event => event.guid == folderNode.bookmarkGuid) + ); + + // Press the delete key and check that the bookmark has been removed. + gLibrary.document.getElementById("placesList").focus(); + EventUtils.synthesizeKey("VK_DELETE", {}, gLibrary); + + await promiseItemRemovedNotification; + + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ url: TEST_URL })), + "Bookmark has been correctly removed" + ); + // Test live update. + Assert.equal(unsortedNode.childCount, 1, "Unsorted node has 1 child"); + Assert.equal(PO._places.selectedNode.title, "keepme", "Folder node selected"); +}); + +add_task(async function test_ensure_correct_selection_and_functionality() { + let PO = gLibrary.PlacesOrganizer; + let ContentTree = gLibrary.ContentTree; + // Move selection forth and back. + PO.selectLeftPaneBuiltIn("History"); + PO.selectLeftPaneBuiltIn("UnfiledBookmarks"); + // Now select the "keepme" folder in the right pane and delete it. + ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0)); + Assert.equal( + ContentTree.view.selectedNode.title, + "keepme", + "Found folder in content pane" + ); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bm", + url: TEST_URL, + }); + + Assert.equal( + ContentTree.view.result.root.childCount, + 2, + "Right pane was correctly updated" + ); +}); + +add_task(async function test_repeated_remove_bookmark() { + // Select and open the left pane "History" query. + let PO = gLibrary.PlacesOrganizer; + PO.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + gLibrary.document.getElementById("placesList").focus(); + let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode); + Assert.equal(unsortedNode.childCount, 1, "Unsorted node has 1 child"); + let folderNode = unsortedNode.getChild(0); + Assert.equal( + folderNode.title, + "keepme", + "Folder found in unsorted bookmarks" + ); + PO._places.selectNode(folderNode); + + Assert.ok( + PO._places.controller.isCommandEnabled("cmd_delete"), + "Delete command is enabled" + ); + registerCleanupFunction(sinon.restore); + let spy = sinon.spy(PO._places.controller, "remove"); + let stub = sinon + .stub(PO._places.controller, "_removeRowsFromBookmarks") + .resolves(); + PO._places.controller.doCommand("cmd_delete"); + PO._places.controller.doCommand("cmd_delete"); + PO._places.controller.doCommand("cmd_delete"); + Assert.equal(spy.callCount, 3, "Should have been invoked thrice"); + Assert.equal(stub.callCount, 1, "Should have been invoked once"); + // Executing another command allows delete to go through again. + PO._places.controller.doCommand("cmd_cut"); + PO._places.controller.doCommand("cmd_delete"); + Assert.equal(spy.callCount, 4, "Should have been invoked again"); + Assert.equal(stub.callCount, 2, "Should have been invoked again"); +}); diff --git a/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js b/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js new file mode 100644 index 0000000000..ed124a047a --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js @@ -0,0 +1,111 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test deleting bookmarks from within tags. + */ + +registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_tags() { + const uris = [ + Services.io.newURI("http://example.com/1"), + Services.io.newURI("http://example.com/2"), + Services.io.newURI("http://example.com/3"), + ]; + + let children = uris.map((uri, index, arr) => { + return { + title: `bm${index}`, + url: uri, + }; + }); + + // Note: we insert the uris in reverse order, so that we end up with the + // display in "logical" order of bm0 at the top, and bm2 at the bottom. + children = children.reverse(); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children, + }); + + for (let uri of uris) { + PlacesUtils.tagging.tagURI(uri, ["test"]); + } + + let library = await promiseLibrary(); + + // Select and open the left pane "Bookmarks Toolbar" folder. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("Tags"); + let tagsNode = PO._places.selectedNode; + Assert.notEqual(tagsNode, null, "Should have a valid selection"); + let tagsTitle = PlacesUtils.getString("TagsFolderTitle"); + Assert.equal(tagsNode.title, tagsTitle, "Should have selected the Tags node"); + + // Now select the tag. + PlacesUtils.asContainer(tagsNode).containerOpen = true; + let tag = tagsNode.getChild(0); + PO._places.selectNode(tag); + Assert.equal( + PO._places.selectedNode.title, + "test", + "Should have selected the created tag" + ); + + let ContentTree = library.ContentTree; + + for (let i = 0; i < uris.length; i++) { + ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0)); + + Assert.equal( + ContentTree.view.selectedNode.title, + `bm${i}`, + `Should have selected bm${i}` + ); + + let promiseNotification = PlacesTestUtils.waitForNotification( + "bookmark-tags-changed" + ); + + ContentTree.view.controller.doCommand("cmd_delete"); + + await promiseNotification; + + for (let j = 0; j < uris.length; j++) { + let tags = PlacesUtils.tagging.getTagsForURI(uris[j]); + if (j <= i) { + Assert.equal( + tags.length, + 0, + `There should be no tags for the URI: ${uris[j].spec}` + ); + } else { + Assert.equal( + tags.length, + 1, + `There should be one tag for the URI: ${uris[j].spec}` + ); + } + } + } + + // The tag should now not exist. + Assert.equal( + await PlacesUtils.bookmarks.fetch({ tags: ["test"] }), + null, + "There should be no URIs remaining for the tag" + ); + + tagsNode.containerOpen = false; + + library.close(); +}); diff --git a/browser/components/places/tests/browser/browser_library_delete_tags.js b/browser/components/places/tests/browser/browser_library_delete_tags.js new file mode 100644 index 0000000000..d5f3eafc63 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_delete_tags.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test enabled commands in the left pane folder of the Library. + */ + +registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_tags() { + const TEST_URI = Services.io.newURI("http://example.com/"); + + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: TEST_URI, + title: "example page", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + PlacesUtils.tagging.tagURI(TEST_URI, ["test"]); + + let library = await promiseLibrary(); + + // Select and open the left pane "Bookmarks Toolbar" folder. + let PO = library.PlacesOrganizer; + + PO.selectLeftPaneBuiltIn("Tags"); + let tagsNode = PO._places.selectedNode; + Assert.notEqual(tagsNode, null, "Should have a valid selection"); + let tagsTitle = PlacesUtils.getString("TagsFolderTitle"); + Assert.equal(tagsNode.title, tagsTitle, "Should have selected the Tags node"); + + // Now select the tag. + PlacesUtils.asContainer(tagsNode).containerOpen = true; + let tag = tagsNode.getChild(0); + PO._places.selectNode(tag); + Assert.equal( + PO._places.selectedNode.title, + "test", + "Should have selected the created tag" + ); + + PO._places.controller.doCommand("cmd_delete"); + + await PlacesTestUtils.promiseAsyncUpdates(); + + let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI); + + Assert.equal(tags.length, 0, "There should be no tags for the URI"); + + tagsNode.containerOpen = false; + + library.close(); +}); diff --git a/browser/components/places/tests/browser/browser_library_downloads.js b/browser/components/places/tests/browser/browser_library_downloads.js new file mode 100644 index 0000000000..3aa37b1fed --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_downloads.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests bug 564900: Add folder specifically for downloads to Library left pane. + * https://bugzilla.mozilla.org/show_bug.cgi?id=564900 + * This test visits various pages then opens the Library and ensures + * that both the Downloads folder shows up and that the correct visits + * are shown in it. + */ + +add_task(async function test() { + // Add visits. + await PlacesTestUtils.addVisits([ + { + uri: "http://mozilla.org", + transition: PlacesUtils.history.TRANSITION_TYPED, + }, + { + uri: "http://google.com", + transition: PlacesUtils.history.TRANSITION_DOWNLOAD, + }, + { + uri: "http://en.wikipedia.org", + transition: PlacesUtils.history.TRANSITION_TYPED, + }, + { + uri: "http://ubuntu.org", + transition: PlacesUtils.history.TRANSITION_DOWNLOAD, + }, + ]); + + let library = await promiseLibrary("Downloads"); + + registerCleanupFunction(async () => { + await library.close(); + await PlacesUtils.history.clear(); + }); + + // Make sure Downloads is present. + Assert.notEqual( + library.PlacesOrganizer._places.selectedNode, + null, + "Downloads is present and selected" + ); + + // Check results. + let testURIs = ["http://ubuntu.org/", "http://google.com/"]; + + await TestUtils.waitForCondition( + () => + library.ContentArea.currentView.associatedElement.itemChildren.length == + testURIs.length + ); + + for (let element of library.ContentArea.currentView.associatedElement + .itemChildren) { + Assert.equal( + element._shell.download.source.url, + testURIs.shift(), + "URI matches" + ); + } +}); diff --git a/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js b/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js new file mode 100644 index 0000000000..fc33963199 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js @@ -0,0 +1,106 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests middle-clicking items in the Library. + */ + +const URIs = ["about:license", "about:mozilla"]; + +var gLibrary = null; + +add_task(async function test_setup() { + // Temporary disable history, so we won't record pages navigation. + await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] }); + + // Open Library window. + gLibrary = await promiseLibrary(); + + registerCleanupFunction(async () => { + // We must close "Other Bookmarks" ready for other tests. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); + + // Close Library window. + await promiseLibraryClosed(gLibrary); + }); +}); + +add_task(async function test_open_folder_in_tabs() { + let children = URIs.map(url => { + return { + title: "Title", + url, + }; + }); + + // Create a new folder. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children, + }, + ], + }); + + // Select unsorted bookmarks root in the left pane. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + Assert.notEqual( + gLibrary.PlacesOrganizer._places.selectedNode, + null, + "We correctly have selection in the Library left pane" + ); + + // Get our bookmark in the right pane. + var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal(folderNode.title, "Folder", "Found folder in the right pane"); + + gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = true; + + // Now middle-click on the bookmark contained with it. + let promiseLoaded = Promise.all( + URIs.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true)) + ); + + let bookmarkedNode = + gLibrary.PlacesOrganizer._places.selectedNode.getChild(0); + mouseEventOnCell( + gLibrary.PlacesOrganizer._places, + gLibrary.PlacesOrganizer._places.view.treeIndexForNode(bookmarkedNode), + 0, + { button: 1 } + ); + + let tabs = await promiseLoaded; + + Assert.ok(true, "Expected tabs were loaded"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) { + var selection = aTree.view.selection; + selection.select(aRowIndex); + aTree.ensureRowIsVisible(aRowIndex); + var column = aTree.columns[aColumnIndex]; + + // get cell coordinates + var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text"); + + EventUtils.synthesizeMouse( + aTree.body, + rect.x, + rect.y, + aEventDetails, + gLibrary + ); +} diff --git a/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js new file mode 100644 index 0000000000..6caaf5a9d4 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function () { + let hierarchy = ["AllBookmarks", "BookmarksMenu"]; + + let items = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + title: "Folder 1", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "Folder 2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "Bookmark", + url: "http://example.com", + }, + ], + }, + ], + }, + ], + }); + + hierarchy.push(items[0].guid, items[1].guid); + + let library = await promiseLibrary(); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(items[0]); + await promiseLibraryClosed(library); + }); + + library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(hierarchy); + + Assert.equal( + library.PlacesOrganizer._places.selectedNode.bookmarkGuid, + items[1].guid, + "Found the expected left pane selected node" + ); + + Assert.equal( + library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid, + items[2].guid, + "Found the expected right pane contents" + ); +}); diff --git a/browser/components/places/tests/browser/browser_library_middleclick.js b/browser/components/places/tests/browser/browser_library_middleclick.js new file mode 100644 index 0000000000..8eb0bfa008 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_middleclick.js @@ -0,0 +1,234 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests middle-clicking items in the Library. + */ + +const URIs = ["about:license", "about:mozilla"]; + +var gLibrary = null; +var gTests = []; + +add_task(async function test_setup() { + // Increase timeout, this test can be quite slow due to waitForFocus calls. + requestLongerTimeout(2); + + // Temporary disable history, so we won't record pages navigation. + await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] }); + + // Ensure the database is empty. + await PlacesUtils.bookmarks.eraseEverything(); + + // Open Library window. + gLibrary = await promiseLibrary(); + + registerCleanupFunction(async () => { + // We must close "Other Bookmarks" ready for other tests. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); + + // Close Library window. + await promiseLibraryClosed(gLibrary); + }); +}); + +gTests.push({ + desc: "Open bookmark in a new tab.", + URIs: ["about:buildconfig"], + _bookmark: null, + + async setup() { + // Add a new unsorted bookmark. + this._bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Title", + url: this.URIs[0], + }); + + // Select unsorted bookmarks root in the left pane. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + Assert.notEqual( + gLibrary.PlacesOrganizer._places.selectedNode, + null, + "We correctly have selection in the Library left pane" + ); + + // Get our bookmark in the right pane. + var bookmarkNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal( + bookmarkNode.uri, + this.URIs[0], + "Found bookmark in the right pane" + ); + }, + + async cleanup() { + await PlacesUtils.bookmarks.remove(this._bookmark); + }, +}); + +// ------------------------------------------------------------------------------ +// Open a folder in tabs. +// +gTests.push({ + desc: "Open a folder in tabs.", + URIs: ["about:buildconfig", "about:mozilla"], + _bookmarks: null, + + async setup() { + // Create a new folder. + let children = this.URIs.map(url => { + return { + title: "Title", + url, + }; + }); + + this._bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children, + }, + ], + }); + + // Select unsorted bookmarks root in the left pane. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + isnot( + gLibrary.PlacesOrganizer._places.selectedNode, + null, + "We correctly have selection in the Library left pane" + ); + // Get our bookmark in the right pane. + var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + is(folderNode.title, "Folder", "Found folder in the right pane"); + }, + + async cleanup() { + await PlacesUtils.bookmarks.remove(this._bookmarks[0]); + }, +}); + +// ------------------------------------------------------------------------------ +// Open a query in tabs. + +gTests.push({ + desc: "Open a query in tabs.", + URIs: ["about:buildconfig", "about:mozilla"], + _bookmarks: null, + _query: null, + + async setup() { + let children = this.URIs.map(url => { + return { + title: "Title", + url, + }; + }); + + this._bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children, + }, + ], + }); + + // Create a bookmarks query containing our bookmarks. + var hs = PlacesUtils.history; + var options = hs.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + var query = hs.getNewQuery(); + // The colon included in the terms selects only about: URIs. If not included + // we also may get pages like about.html included in the query result. + query.searchTerms = "about:"; + var queryString = hs.queryToQueryString(query, options); + this._query = await PlacesUtils.bookmarks.insert({ + index: 0, // it must be the first + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Query", + url: queryString, + }); + + // Select unsorted bookmarks root in the left pane. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + isnot( + gLibrary.PlacesOrganizer._places.selectedNode, + null, + "We correctly have selection in the Library left pane" + ); + // Get our bookmark in the right pane. + var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + is(folderNode.title, "Query", "Found query in the right pane"); + }, + + async cleanup() { + await PlacesUtils.bookmarks.remove(this._bookmarks[0]); + await PlacesUtils.bookmarks.remove(this._query); + }, +}); + +async function runTest(test) { + info("Start of test: " + test.desc); + // Test setup will set Library so that the bookmark to be opened is the + // first node in the content (right pane) tree. + await test.setup(); + + // Middle click on first node in the content tree of the Library. + gLibrary.focus(); + await SimpleTest.promiseFocus(gLibrary); + + // Now middle-click on the bookmark contained with it. + let promiseLoaded = Promise.all( + test.URIs.map(uri => + BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true) + ) + ); + + mouseEventOnCell(gLibrary.ContentTree.view, 0, 0, { button: 1 }); + + let tabs = await promiseLoaded; + + Assert.ok(true, "Expected tabs were loaded"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + + await test.cleanup(); +} + +add_task(async function test_all() { + for (let test of gTests) { + await runTest(test); + } +}); + +function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) { + var selection = aTree.view.selection; + selection.select(aRowIndex); + aTree.ensureRowIsVisible(aRowIndex); + var column = aTree.columns[aColumnIndex]; + + // get cell coordinates + var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text"); + + EventUtils.synthesizeMouse( + aTree.body, + rect.x, + rect.y, + aEventDetails, + gLibrary + ); +} diff --git a/browser/components/places/tests/browser/browser_library_new_bookmark.js b/browser/components/places/tests/browser/browser_library_new_bookmark.js new file mode 100644 index 0000000000..dff7accc44 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_new_bookmark.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the a new bookmark is correctly selected after being created via + * the bookmark dialog. + */ +"use strict"; + +let bookmarks = [ + { + url: "https://example1.com", + title: "bm1", + }, + { + url: "https://example2.com", + title: "bm2", + }, + { + url: "https://example3.com", + title: "bm3", + }, +]; + +add_task(async function test_open_bookmark_from_library() { + let bm = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarks, + }); + + let library = await promiseLibrary("UnfiledBookmarks"); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + let bmLibrary = library.ContentTree.view.view.nodeForTreeIndex(1); + Assert.equal( + bmLibrary.title, + bm[1].title, + "EditBookmark: Found bookmark in the right pane" + ); + + library.ContentTree.view.selectNode(bmLibrary); + + let beforeUpdatedPRTime; + await withBookmarksDialog( + false, + async () => { + // Open the context menu. + let placesContext = library.document.getElementById("placesContext"); + let promisePopup = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + await promisePopup; + let properties = library.document.getElementById( + "placesContext_new:bookmark" + ); + placesContext.activateItem(properties, {}); + }, + async dialogWin => { + beforeUpdatedPRTime = Date.now() * 1000; + + fillBookmarkTextField( + "editBMPanel_locationField", + "https://example4.com/", + dialogWin, + false + ); + + EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin); + } + ); + let node = library.ContentTree.view.selectedNode; + Assert.ok(node, "EditBookmark: Should have a selectedNode"); + Assert.equal( + node.uri, + "https://example4.com/", + "EditBookmark: Should have selected the newly created bookmark" + ); + Assert.greater( + node.lastModified, + beforeUpdatedPRTime, + "EditBookmark: The lastModified should be greater than the time of before updating" + ); + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/places/tests/browser/browser_library_openFlatContainer.js b/browser/components/places/tests/browser/browser_library_openFlatContainer.js new file mode 100644 index 0000000000..1d9cc61f2f --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_openFlatContainer.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test opening a flat container in the right pane even if its parent in the + * left pane is closed. + */ + +var library; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await promiseLibraryClosed(library); + }); +}); + +add_task(async function test_open_built_in_folder() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "testBM", + url: "http://example.com/1", + }); + + library = await promiseLibrary("AllBookmarks"); + + library.ContentTree.view.selectItems([PlacesUtils.bookmarks.menuGuid]); + + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + clickCount: 2, + }); + + Assert.equal( + library.PlacesOrganizer._places.selectedNode.bookmarkGuid, + PlacesUtils.bookmarks.virtualMenuGuid, + "Should have the bookmarks menu selected in the left pane." + ); + Assert.equal( + library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid, + bm.guid, + "Should have the expected bookmark selected in the right pane" + ); +}); + +add_task(async function test_open_new_folder_in_unfiled() { + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "Bookmark", + url: "http://example.com", + }, + ], + }, + ], + }); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + // Ensure the container is closed. + library.PlacesOrganizer._places.selectedNode.containerOpen = false; + + let folderNode = library.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal( + folderNode.bookmarkGuid, + bookmarks[0].guid, + "Found the expected folder in the right pane" + ); + // Select the folder node in the right pane. + library.ContentTree.view.selectNode(folderNode); + + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + clickCount: 2, + }); + + Assert.equal( + library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid, + bookmarks[1].guid, + "Found the expected bookmark in the right pane" + ); +}); + +add_task(async function test_open_history_query() { + const todayTitle = PlacesUtils.getString("finduri-AgeInDays-is-0"); + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com", + title: "Whittingtons", + }, + ]); + + library.PlacesOrganizer.selectLeftPaneBuiltIn("History"); + + // Ensure the container is closed. + library.PlacesOrganizer._places.selectedNode.containerOpen = false; + + let query = library.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal(query.title, todayTitle, "Should have the today query"); + + library.ContentTree.view.selectNode(query); + + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + clickCount: 2, + }); + + Assert.equal( + library.PlacesOrganizer._places.selectedNode.title, + todayTitle, + "Should have selected the today query in the left-pane." + ); + Assert.equal( + library.ContentTree.view.view.nodeForTreeIndex(0).title, + "Whittingtons", + "Found the expected history item in the right pane" + ); +}); diff --git a/browser/components/places/tests/browser/browser_library_open_all.js b/browser/components/places/tests/browser/browser_library_open_all.js new file mode 100644 index 0000000000..18f0014936 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_open_all.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + const TEST_EXAMPLE_URL = "http://example.com/"; + const TEST_EXAMPLE_PARAMS = "?foo=1|2"; + const TEST_EXAMPLE_TITLE = "Example Domain"; + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_EXAMPLE_URL + TEST_EXAMPLE_PARAMS, + title: TEST_EXAMPLE_TITLE, + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_EXAMPLE_URL, + title: TEST_EXAMPLE_TITLE, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_open_all_in_tabs_from_library() { + let gLibrary = await promiseLibrary("AllBookmarks"); + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + gLibrary.ContentTree.view.selectAll(); + let placesContext = gLibrary.document.getElementById("placesContext"); + let promiseContextMenu = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + await promiseContextMenu; + let openTabs = gLibrary.document.getElementById( + "placesContext_openBookmarkLinks:tabs" + ); + let promiseWaitForWindow = BrowserTestUtils.waitForNewWindow(); + placesContext.activateItem(openTabs, { shiftKey: true }); + let newWindow = await promiseWaitForWindow; + + Assert.equal( + newWindow.browserDOMWindow.tabCount, + 2, + "Expected number of tabs opened in new window" + ); + + await BrowserTestUtils.closeWindow(newWindow); + await promiseLibraryClosed(gLibrary); +}); diff --git a/browser/components/places/tests/browser/browser_library_open_all_with_separator.js b/browser/components/places/tests/browser/browser_library_open_all_with_separator.js new file mode 100644 index 0000000000..a68158a1ba --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_open_all_with_separator.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + title: "Example One", + url: "https://example.com/1/", + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + title: "Example Two", + url: "https://example.com/2/", + }, + ], + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_open_all_without_separator() { + let gLibrary = await promiseLibrary("AllBookmarks"); + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + gLibrary.ContentTree.view.selectAll(); + + let placesContext = gLibrary.document.getElementById("placesContext"); + let promiseContextMenu = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + await promiseContextMenu; + + let openTabs = gLibrary.document.getElementById( + "placesContext_openBookmarkLinks:tabs" + ); + let promiseWaitForWindow = BrowserTestUtils.waitForNewWindow(); + placesContext.activateItem(openTabs, { shiftKey: true }); + let newWindow = await promiseWaitForWindow; + + Assert.equal( + newWindow.browserDOMWindow.tabCount, + 2, + "Expected number of tabs opened in new window" + ); + + await BrowserTestUtils.closeWindow(newWindow); + await promiseLibraryClosed(gLibrary); +}); diff --git a/browser/components/places/tests/browser/browser_library_open_bookmark.js b/browser/components/places/tests/browser/browser_library_open_bookmark.js new file mode 100644 index 0000000000..7532d7c1c9 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_open_bookmark.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test that the a bookmark can be opened from the Library by mouse double click. + */ +"use strict"; + +const TEST_URL = "about:buildconfig"; + +add_task(async function test_open_bookmark_from_library() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: TEST_URL, + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let gLibrary = await promiseLibrary("UnfiledBookmarks"); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(gLibrary); + await PlacesUtils.bookmarks.eraseEverything(); + await BrowserTestUtils.removeTab(tab); + }); + + let bmLibrary = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal(bmLibrary.title, bm.title, "Found bookmark in the right pane"); + + gLibrary.ContentTree.view.selectNode(bmLibrary); + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + clickCount: 2, + }); + + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + Assert.ok(true, "Expected tab was loaded"); +}); diff --git a/browser/components/places/tests/browser/browser_library_open_leak.js b/browser/components/places/tests/browser/browser_library_open_leak.js new file mode 100644 index 0000000000..02a0874c76 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_open_leak.js @@ -0,0 +1,22 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 474831 + * https://bugzilla.mozilla.org/show_bug.cgi?id=474831 + * + * Tests for leaks caused by simply opening and closing the Places Library + * window. Opens the Places Library window, waits for it to load, closes it, + * and finishes. + */ + +add_task(async function test_open_and_close() { + let library = await promiseLibrary(); + + Assert.ok(true, "Library has been correctly opened"); + + await promiseLibraryClosed(library); +}); diff --git a/browser/components/places/tests/browser/browser_library_panel_leak.js b/browser/components/places/tests/browser/browser_library_panel_leak.js new file mode 100644 index 0000000000..f8b536cecc --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_panel_leak.js @@ -0,0 +1,71 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 433231 - Places Library leaks the nsGlobalWindow when closed with a + * history entry selected. + * https://bugzilla.mozilla.org/show_bug.cgi?id=433231 + * + * STRs: Open Library, select an history entry in History, close Library. + * ISSUE: We were adding a bookmarks observer when editing a bookmark, when + * selecting an history entry the panel was not un-initialized, and + * since an history entry does not have an itemId, the observer was + * never removed. + */ + +const TEST_URI = "http://www.mozilla.org/"; + +add_task(async function test_no_leak_closing_library_with_history_selected() { + // Add an history entry. + await PlacesTestUtils.addVisits(TEST_URI); + + let organizer = await promiseLibrary(); + + let contentTree = organizer.document.getElementById("placeContent"); + Assert.notEqual( + contentTree, + null, + "Sanity check: placeContent tree should exist" + ); + Assert.notEqual( + organizer.PlacesOrganizer, + null, + "Sanity check: PlacesOrganizer should exist" + ); + Assert.notEqual( + organizer.gEditItemOverlay, + null, + "Sanity check: gEditItemOverlay should exist" + ); + + Assert.ok( + organizer.gEditItemOverlay.initialized, + "gEditItemOverlay is initialized" + ); + Assert.notEqual( + organizer.gEditItemOverlay._paneInfo.itemGuid, + "", + "Editing a bookmark" + ); + + // Select History in the left pane. + organizer.PlacesOrganizer.selectLeftPaneBuiltIn("History"); + // Select the first history entry. + let selection = contentTree.view.selection; + selection.clearSelection(); + selection.rangedSelect(0, 0, true); + // Check the panel is editing the history entry. + Assert.equal( + organizer.gEditItemOverlay._paneInfo.itemGuid, + "", + "Editing an history entry" + ); + // Close Library window. + organizer.close(); + + // Clean up history. + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js b/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js new file mode 100644 index 0000000000..d9b800697d --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const sandbox = sinon.createSandbox(); + +/** + * This checks an optimization in the Library code, that tries to not update + * the details pane if the same node that is being edited is picked again. + * That happens for example if the focus moves to the details pane and back + * to the tree. + */ + +add_task(async function () { + let bm1 = await PlacesUtils.bookmarks.insert({ + url: "https://bookmark1.mozilla.org/", + title: "Bookmark 1", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + let library = await promiseLibrary("UnfiledBookmarks"); + registerCleanupFunction(async () => { + sandbox.restore(); + await promiseLibraryClosed(library); + await PlacesUtils.bookmarks.remove(bm1); + }); + + let nameField = library.document.getElementById("editBMPanel_namePicker"); + let tree = library.ContentTree.view; + tree.selectItems([bm1.guid]); + Assert.equal(tree.selectedNode.title, "Bookmark 1"); + await synthesizeClickOnSelectedTreeCell(tree); + Assert.equal(nameField.value, "Bookmark 1"); + + let updateSpy = sandbox.spy(library.PlacesOrganizer, "updateDetailsPane"); + let uninitSpy = sandbox.spy(library.gEditItemOverlay, "uninitPanel"); + nameField.focus(); + await synthesizeClickOnSelectedTreeCell(tree); + Assert.ok(updateSpy.calledOnce, "should try to update the details pane"); + Assert.ok(uninitSpy.notCalled, "should skip the update cause same node"); +}); diff --git a/browser/components/places/tests/browser/browser_library_search.js b/browser/components/places/tests/browser/browser_library_search.js new file mode 100644 index 0000000000..898f664269 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_search.js @@ -0,0 +1,206 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 451151 + * https://bugzilla.mozilla.org/show_bug.cgi?id=451151 + * + * Summary: + * Tests frontend Places Library searching -- search, search reset, search scope + * consistency. + * + * Details: + * Each test below + * 1. selects a folder in the left pane and ensures that the content tree is + * appropriately updated, + * 2. performs a search and ensures that the content tree is correct for the + * folder and search and that the search UI is visible and appropriate to + * folder, + * 5. resets the search and ensures that the content tree is correct and that + * the search UI is hidden, and + * 6. if folder scope was clicked, searches again and ensures folder scope + * remains selected. + */ + +const TEST_URL = "http://dummy.mozilla.org/"; +const TEST_DOWNLOAD_URL = "http://dummy.mozilla.org/dummy.pdf"; +const TEST_PARENT_FOLDER = "testParentFolder"; +const TEST_SIF_URL = "http://testsif.example.com/"; +const TEST_SIF_TITLE = "TestSIF"; + +var gLibrary; + +/** + * Performs a search for a given folder and search string and ensures that the + * URI of the right pane's content tree is as expected for the folder and search + * string. Also ensures that the search scope button is as expected after the + * search. + * + * @param {string} aFolderGuid + * the item guid of a node in the left pane's tree + * @param {string} aSearchStr + * the search text; may be empty to reset the search + */ +async function search(aFolderGuid, aSearchStr) { + let doc = gLibrary.document; + let folderTree = doc.getElementById("placesList"); + let contentTree = doc.getElementById("placeContent"); + + // First, ensure that selecting the folder in the left pane updates the + // content tree properly. + if (aFolderGuid) { + folderTree.selectItems([aFolderGuid]); + Assert.notEqual( + folderTree.selectedNode, + null, + "Sanity check: left pane tree should have selection after selecting!" + ); + + // The downloads folder never quite matches the url of the contentTree, + // probably due to the way downloads are loaded. + if (aFolderGuid !== PlacesUtils.virtualDownloadsGuid) { + Assert.equal( + folderTree.selectedNode.uri, + contentTree.place, + "Content tree's folder should be what was selected in the left pane" + ); + } + } + + // Second, ensure that searching updates the content tree and search UI + // properly. + let searchBox = doc.getElementById("searchFilter"); + searchBox.value = aSearchStr; + gLibrary.PlacesSearchBox.search(searchBox.value); + let query = {}; + PlacesUtils.history.queryStringToQuery( + contentTree.result.root.uri, + query, + {} + ); + if (aSearchStr) { + Assert.equal( + query.value.searchTerms, + aSearchStr, + "Content tree's searchTerms should be text in search box" + ); + } else { + Assert.equal( + query.value.hasSearchTerms, + false, + "Content tree's searchTerms should not exist after search reset" + ); + } +} + +async function showInFolder(aFolderGuid, aSearchStr, aParentFolderGuid) { + let doc = gLibrary.document; + let folderTree = doc.getElementById("placesList"); + let contentTree = doc.getElementById("placeContent"); + + let searchBox = doc.getElementById("searchFilter"); + searchBox.value = aSearchStr; + gLibrary.PlacesSearchBox.search(searchBox.value); + let theNode = contentTree.view._getNodeForRow(0); + let bookmarkGuid = theNode.bookmarkGuid; + + Assert.equal(theNode.uri, TEST_SIF_URL, "Found expected bookmark"); + + contentTree.selectNode(theNode); + info("Executing showInFolder"); + info("Waiting for showInFolder to select folder in tree"); + let folderSelected = BrowserTestUtils.waitForEvent(folderTree, "select"); + contentTree.controller.doCommand("placesCmd_showInFolder"); + await folderSelected; + + let treeNode = folderTree.selectedNode; + let contentNode = contentTree.selectedNode; + Assert.equal( + treeNode.bookmarkGuid, + aParentFolderGuid, + "Containing folder node selected on left tree pane" + ); + Assert.equal( + contentNode.bookmarkGuid, + bookmarkGuid, + "The searched bookmark guid matches selected node in content pane" + ); + Assert.equal( + contentNode.uri, + TEST_SIF_URL, + "The searched bookmark URL matches selected node in content pane" + ); +} + +add_task(async function test() { + // Add visits, a bookmark and a tag. + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI(TEST_URL), + visitDate: Date.now() * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED, + }, + { + uri: Services.io.newURI(TEST_DOWNLOAD_URL), + visitDate: Date.now() * 1000, + transition: PlacesUtils.history.TRANSITION_DOWNLOAD, + }, + ]); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "dummy", + url: TEST_URL, + }); + + PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["dummyTag"]); + + gLibrary = await promiseLibrary(); + + const rootsToTest = [ + PlacesUtils.virtualAllBookmarksGuid, + PlacesUtils.virtualHistoryGuid, + PlacesUtils.virtualDownloadsGuid, + ]; + + for (let root of rootsToTest) { + await search(root, "dummy"); + } + + await promiseLibraryClosed(gLibrary); + + // Cleanup before testing Show in Folder. + PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["dummyTag"]); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + // Now test Show in Folder + gLibrary = await promiseLibrary(); + info("Test Show in Folder"); + let parentFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: TEST_PARENT_FOLDER, + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: parentFolder.guid, + title: TEST_SIF_TITLE, + url: TEST_SIF_URL, + }); + + await showInFolder( + PlacesUtils.virtualAllBookmarksGuid, + TEST_SIF_TITLE, + parentFolder.guid + ); + + // Cleanup + await promiseLibraryClosed(gLibrary); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/places/tests/browser/browser_library_tags_visibility.js b/browser/components/places/tests/browser/browser_library_tags_visibility.js new file mode 100644 index 0000000000..d0427ef591 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_tags_visibility.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test whether tags are shown in library window. + */ + +registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +const TEST_URI = Services.io.newURI("https://example.com/"); +const TEST_TAGS = ["tagB", "tagA"]; +const TAGS_TEXT = TEST_TAGS.sort().join(", "); + +add_task(async function base() { + const { guid } = await PlacesUtils.bookmarks.insert({ + url: TEST_URI, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesTestUtils.addVisits(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, TEST_TAGS); + + const library = await promiseLibrary(); + registerCleanupFunction(() => { + library.close(); + }); + + const { + document: libraryDocument, + PlacesOrganizer: placesOrganizer, + ContentTree: contentTree, + } = library; + + info("Test in Bookmarks"); + placesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + contentTree.view.selectItems([guid]); + await assertTagsVisibility(libraryDocument, true); + + info("Test in Tags"); + placesOrganizer.selectLeftPaneBuiltIn("Tags"); + const tagsContainer = PlacesUtils.asContainer( + placesOrganizer._places.selectedNode + ); + tagsContainer.containerOpen = true; + const targetNode = tagsContainer.getChild(0); + placesOrganizer._places.selectNode(targetNode); + contentTree.view.selectItems([guid]); + await assertTagsVisibility(libraryDocument, true); + + info("Test in History"); + placesOrganizer.selectLeftPaneBuiltIn("History"); + const historyNode = PlacesUtils.asContainer( + placesOrganizer._places.selectedNode + ); + historyNode.containerOpen = true; + const todayNode = historyNode.getChild(0); + placesOrganizer._places.selectNode(todayNode); + contentTree.view.selectItems([guid]); + await assertTagsVisibility(libraryDocument, false); +}); + +async function assertTagsVisibility(libraryDocument, expectedVisible) { + const locationInput = libraryDocument.getElementById( + "editBMPanel_locationField" + ); + await BrowserTestUtils.waitForCondition( + () => locationInput.value === TEST_URI.spec, + "Wait until the panel ready" + ); + + // Check the editor area. + const tagInput = libraryDocument.getElementById("editBMPanel_tagsField"); + Assert.equal(BrowserTestUtils.isVisible(tagInput), expectedVisible); + if (expectedVisible) { + Assert.equal(tagInput.value, TAGS_TEXT); + } + + // Check the cell. + const tree = libraryDocument.getElementById("placeContent"); + const expectedCellText = expectedVisible ? TAGS_TEXT : null; + Assert.equal( + tree.view.getCellText(0, tree.columns.placesContentTags), + expectedCellText + ); +} diff --git a/browser/components/places/tests/browser/browser_library_telemetry.js b/browser/components/places/tests/browser/browser_library_telemetry.js new file mode 100644 index 0000000000..00ca4635d8 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_telemetry.js @@ -0,0 +1,413 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +// Visited pages listed by descending visit date. +const pages = [ + "https://library.mozilla.org/a", + "https://library.mozilla.org/b", + "https://library.mozilla.org/c", + "https://www.mozilla.org/d", +]; + +// The prompt returns 1 for cancelled and 0 for accepted. +let gResponse = 1; +(function replacePromptService() { + let originalPromptService = Services.prompt; + Services.prompt = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]), + confirmEx: () => gResponse, + }; + registerCleanupFunction(() => { + Services.prompt = originalPromptService; + }); +})(); + +async function searchHistory(gLibrary, searchTerm) { + let doc = gLibrary.document; + let contentTree = doc.getElementById("placeContent"); + + let searchBox = doc.getElementById("searchFilter"); + searchBox.value = searchTerm; + gLibrary.PlacesSearchBox.search(searchBox.value); + let query = {}; + PlacesUtils.history.queryStringToQuery( + contentTree.result.root.uri, + query, + {} + ); + Assert.equal( + query.value.searchTerms, + searchTerm, + "Content tree's searchTerms should be text in search box" + ); +} + +function searchBookmarks(gLibrary, searchTerm) { + let searchBox = gLibrary.document.getElementById("searchFilter"); + searchBox.value = searchTerm; + gLibrary.PlacesSearchBox.search(searchBox.value); +} + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add some visited pages to history + let time = Date.now(); + let places = []; + for (let i = 0; i < pages.length; i++) { + places.push({ + uri: NetUtil.newURI(pages[i]), + visitDate: (time - i) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(places); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "Mozilla", + url: "https://www.mozilla.org/", + }, + { + title: "Example", + url: "https://sidebar.mozilla.org/", + }, + ], + }); + await registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_library_history_telemetry() { + Services.telemetry.clearScalars(); + let cumulativeSearchesHistogram = Services.telemetry.getHistogramById( + "PLACES_LIBRARY_CUMULATIVE_HISTORY_SEARCHES" + ); + + let gLibrary = await promiseLibrary("History"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.opened", + "history", + 1 + ); + + let currentSelectedLeftPaneNode = + gLibrary.PlacesOrganizer._places.selectedNode; + if ( + currentSelectedLeftPaneNode.title == "History" && + currentSelectedLeftPaneNode.hasChildren && + currentSelectedLeftPaneNode.getChild(0).title == "Today" + ) { + // Select "Today" node under History if not already selected + gLibrary.PlacesOrganizer._places.selectNode( + currentSelectedLeftPaneNode.getChild(0) + ); + } + + Assert.equal( + gLibrary.PlacesOrganizer._places.selectedNode.title, + "Today", + "The Today history sidebar button is selected" + ); + + await searchHistory(gLibrary, "mozilla"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.search", + "history", + 1 + ); + + let firstHistoryNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + Assert.equal( + firstHistoryNode.uri, + pages[0], + "Found history item in the right pane" + ); + + // Double click first History link to open it + gLibrary.ContentTree.view.selectNode(firstHistoryNode); + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + clickCount: 2, + }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 1 + ); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1); + info("Cumulative search telemetry looks right"); + + cumulativeSearchesHistogram.clear(); + + // Close and reopen Libary window + await promiseLibraryClosed(gLibrary); + gLibrary = await promiseLibrary("History"); + + Assert.equal( + gLibrary.PlacesOrganizer._places.selectedNode.title, + "Today", + "The Today history sidebar button is selected" + ); + + // if a user tries to open all history entries and they cancel opening + // those entries in new tabs (due to max tab limit warning), + // no telemetry should be recorded + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.maxOpenBeforeWarn", 4]], + }); + + // Reject opening all tabs when prompted + gResponse = 1; + + // Open all history entries + synthesizeClickOnSelectedTreeCell(gLibrary.PlacesOrganizer._places, { + button: 1, + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link" + ); + + // Make 4 searches before opening History links + await searchHistory(gLibrary, "library a", 1); + info("First search was performed."); + await searchHistory(gLibrary, "library b", 2); + info("Second search was performed."); + await searchHistory(gLibrary, "library c", 3); + info("Third search was performed."); + await searchHistory(gLibrary, "mozilla", 4); + info("Fourth search was performed."); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.search", + "history", + 4 + ); + + // Accept opening all tabs when prompted + gResponse = 0; + + // Open all history entries + synthesizeClickOnSelectedTreeCell(gLibrary.PlacesOrganizer._places, { + button: 1, + }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 4 + ); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 4, 1); + info("Cumulative search telemetry looks right"); + + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link" + ); + + let openOption = document.getElementById("placesContext_open"); + openOption.click(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 1 + ); + + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link" + ); + + let openNewTabOption = document.getElementById("placesContext_open:newtab"); + openNewTabOption.click(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 1 + ); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link" + ); + + let openNewWindowOption = document.getElementById( + "placesContext_open:newwindow" + ); + openNewWindowOption.click(); + + let newWin = await newWinOpened; + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 1 + ); + + await BrowserTestUtils.closeWindow(newWin); + + let newPrivateWinOpened = BrowserTestUtils.waitForNewWindow(); + + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link" + ); + + let openNewPrivateWindowOption = document.getElementById( + "placesContext_open:newprivatewindow" + ); + openNewPrivateWindowOption.click(); + + let newPrivateWin = await newPrivateWinOpened; + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "history", + 1 + ); + + await BrowserTestUtils.closeWindow(newPrivateWin); + + cumulativeSearchesHistogram.clear(); + await promiseLibraryClosed(gLibrary); + await SpecialPowers.popPrefEnv(); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); + +add_task(async function test_library_bookmarks_telemetry() { + Services.telemetry.clearScalars(); + let cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram( + "PLACES_LIBRARY_CUMULATIVE_BOOKMARK_SEARCHES" + ); + + let library = await promiseLibrary("AllBookmarks"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.opened", + "bookmarks", + 1 + ); + + searchBookmarks(library, "mozilla"); + + // reset + searchBookmarks(library, ""); + + // search again + searchBookmarks(library, "moz"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.search", + "bookmarks", + 2 + ); + + let firstNode = library.ContentTree.view.view.nodeForTreeIndex(0); + library.ContentTree.view.selectNode(firstNode); + + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + clickCount: 2, + }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "bookmarks", + 1 + ); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1); + + cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram( + "PLACES_LIBRARY_CUMULATIVE_BOOKMARK_SEARCHES" + ); + + // do another search to make sure everything has been cleared + searchBookmarks(library, "moz"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.search", + "bookmarks", + 1 + ); + + firstNode = library.ContentTree.view.view.nodeForTreeIndex(0); + library.ContentTree.view.selectNode(firstNode); + + synthesizeClickOnSelectedTreeCell(library.ContentTree.view, { + clickCount: 2, + }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "library.link", + "bookmarks", + 1 + ); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1); + + cumulativeSearchesHistogram.clear(); + await promiseLibraryClosed(library); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/places/tests/browser/browser_library_tree_leak.js b/browser/components/places/tests/browser/browser_library_tree_leak.js new file mode 100644 index 0000000000..9e552f5c31 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_tree_leak.js @@ -0,0 +1,24 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +add_task(async function bookmark_leak_window() { + // A library window has two trees after selecting a bookmark item: + // A left tree (#placesList) and a right tree (#placeContent). + // Upon closing the window, both trees are destructed, in an unspecified + // order. In bug 1520047, a memory leak was observed when the left tree + // was destroyed last. + + let library = await promiseLibrary("BookmarksToolbar"); + let tree = library.document.getElementById("placesList"); + tree.selectItems(["toolbar_____"]); + + await synthesizeClickOnSelectedTreeCell(tree); + await promiseLibraryClosed(library); + + Assert.ok( + true, + "Closing a window after selecting a node in the tree should not cause a leak" + ); +}); diff --git a/browser/components/places/tests/browser/browser_library_views_liveupdate.js b/browser/components/places/tests/browser/browser_library_views_liveupdate.js new file mode 100644 index 0000000000..ca79768f8f --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.js @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests Library Left pane view for liveupdate. + */ + +let gLibrary = null; + +add_setup(async function () { + gLibrary = await promiseLibrary(); + await PlacesUtils.bookmarks.eraseEverything(); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + await promiseLibraryClosed(gLibrary); + }); +}); + +async function testInFolder(folderGuid, prefix) { + let addedBookmarks = []; + + let item = await insertAndCheckItem( + { + parentGuid: folderGuid, + title: `${prefix}1`, + url: `http://${prefix}1.mozilla.org/`, + }, + 0 + ); + item.title = `${prefix}1_edited`; + await updateAndCheckItem(item, 0); + addedBookmarks.push(item); + + item = await insertAndCheckItem( + { + parentGuid: folderGuid, + title: `${prefix}2`, + url: "place:", + }, + 0 + ); + + item.title = `${prefix}2_edited`; + await updateAndCheckItem(item, 0); + addedBookmarks.push(item); + + item = await insertAndCheckItem( + { + parentGuid: folderGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + 2 + ); + addedBookmarks.push(item); + + item = await insertAndCheckItem( + { + parentGuid: folderGuid, + title: `${prefix}f`, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + 1 + ); + + item.title = `${prefix}f_edited`; + await updateAndCheckItem(item, 1); + + item.index = 0; + await updateAndCheckItem(item, 0); + addedBookmarks.push(item); + + let folderGuid1 = item.guid; + + item = await insertAndCheckItem( + { + parentGuid: folderGuid1, + title: `${prefix}f1`, + url: `http://${prefix}f1.mozilla.org/`, + }, + 0 + ); + addedBookmarks.push(item); + + item = await insertAndCheckItem( + { + parentGuid: folderGuid, + title: `${prefix}f12`, + url: `http://${prefix}f12.mozilla.org/`, + }, + 4 + ); + addedBookmarks.push(item); + + // Move to a different folder and index. + item.parentGuid = folderGuid1; + item.index = 0; + await updateAndCheckItem(item, 0); + + return addedBookmarks; +} + +add_task(async function test() { + let addedBookmarks = []; + + info("*** Acting on menu bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm") + ); + + info("*** Acting on toolbar bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.toolbarGuid, "tb") + ); + + info("*** Acting on unsorted bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.unfiledGuid, "ub") + ); + + // Remove bookmarks in reverse order, so that the effects are correct. + for (let i = addedBookmarks.length - 1; i >= 0; i--) { + await removeAndCheckItem(addedBookmarks[i]); + } +}); + +function selectItem(item, parentTreeIndex) { + let useLeftPane = + item.type == PlacesUtils.bookmarks.TYPE_FOLDER || // is Folder + (item.type == PlacesUtils.bookmarks.TYPE_BOOKMARK && + item.url.protocol == "place:"); // is Query + let tree = useLeftPane + ? gLibrary.PlacesOrganizer._places + : gLibrary.ContentTree.view; + tree.selectItems([item.guid]); + let treeIndex = tree.view.treeIndexForNode(tree.selectedNode); + let title = tree.view.getCellText(treeIndex, tree.columns.getColumnAt(0)); + if (useLeftPane) { + // Make the treeIndex relative to the parent, otherwise getting the right + // index value is tricky due to separators and URIs being hidden in the left + // pane. + treeIndex -= parentTreeIndex + 1; + } + return { + node: tree.selectedNode, + title, + parentRelativeTreeIndex: treeIndex, + }; +} + +function itemExists(item) { + let useLeftPane = + item.type == PlacesUtils.bookmarks.TYPE_FOLDER || // is Folder + (item.type == PlacesUtils.bookmarks.TYPE_BOOKMARK && + item.url.protocol == "place:"); // is Query + let tree = useLeftPane + ? gLibrary.PlacesOrganizer._places + : gLibrary.ContentTree.view; + tree.selectItems([item.guid], true); + return tree.selectedNode?.bookmarkGuid == item.guid; +} + +async function insertAndCheckItem(insertItem, expectedParentRelativeIndex) { + // Ensure the parent is selected before the change, this covers live updating + // better than selecting the parent later, that would just refresh all its + // children. + Assert.ok(insertItem.parentGuid, "Must have a parentGuid"); + gLibrary.PlacesOrganizer._places.selectItems([insertItem.parentGuid], true); + let tree = gLibrary.PlacesOrganizer._places; + let parentTreeIndex = tree.view.treeIndexForNode(tree.selectedNode); + + let item = await PlacesUtils.bookmarks.insert(insertItem); + + let { node, title, parentRelativeTreeIndex } = selectItem( + item, + parentTreeIndex + ); + Assert.equal(item.guid, node.bookmarkGuid, "Should find the updated node"); + Assert.equal(title, item.title, "Should have the correct title"); + Assert.equal( + parentRelativeTreeIndex, + expectedParentRelativeIndex, + "Should have the expected index" + ); + return item; +} + +async function updateAndCheckItem(updateItem, expectedParentRelativeTreeIndex) { + // Ensure the parent is selected before the change, this covers live updating + // better than selecting the parent later, that would just refresh all its + // children. + Assert.ok(updateItem.parentGuid, "Must have a parentGuid"); + gLibrary.PlacesOrganizer._places.selectItems([updateItem.parentGuid], true); + let tree = gLibrary.PlacesOrganizer._places; + let parentTreeIndex = tree.view.treeIndexForNode(tree.selectedNode); + + let item = await PlacesUtils.bookmarks.update(updateItem); + + let { node, title, parentRelativeTreeIndex } = selectItem( + item, + parentTreeIndex + ); + Assert.equal(item.guid, node.bookmarkGuid, "Should find the updated node"); + Assert.equal(title, item.title, "Should have the correct title"); + Assert.equal( + parentRelativeTreeIndex, + expectedParentRelativeTreeIndex, + "Should have the expected index" + ); + return item; +} + +async function removeAndCheckItem(itemData) { + await PlacesUtils.bookmarks.remove(itemData); + Assert.ok(!itemExists(itemData), "Should not find the updated node"); +} diff --git a/browser/components/places/tests/browser/browser_library_warnOnOpen.js b/browser/components/places/tests/browser/browser_library_warnOnOpen.js new file mode 100644 index 0000000000..4289d414df --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_warnOnOpen.js @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Bug 1435562 - Test that browser.tabs.warnOnOpen is respected when + * opening multiple items from the Library. */ + +"use strict"; + +var gLibrary = null; + +add_setup(async function () { + // Temporarily disable history, so we won't record pages navigation. + await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] }); + + // Open Library window. + gLibrary = await promiseLibrary(); + + registerCleanupFunction(async () => { + // We must close "Other Bookmarks" ready for other tests. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false; + + await PlacesUtils.bookmarks.eraseEverything(); + + // Close Library window. + await promiseLibraryClosed(gLibrary); + }); +}); + +add_task(async function test_warnOnOpenFolder() { + // Generate a list of links larger than browser.tabs.maxOpenBeforeWarn + const MAX_LINKS = 16; + let children = []; + for (let i = 0; i < MAX_LINKS; i++) { + children.push({ + title: `Folder Target ${i}`, + url: `http://example${i}.com`, + }); + } + + // Create a new folder containing our links. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bigFolder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children, + }, + ], + }); + info("Pushed test folder into the bookmarks tree"); + + // Select unsorted bookmarks root in the left pane. + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + info("Got selection in the Library left pane"); + + // Get our bookmark in the right pane. + gLibrary.ContentTree.view.view.nodeForTreeIndex(0); + info("Got bigFolder in the right pane"); + + gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = true; + + // Middle-click on folder (opens all links in folder) and then cancel opening in the dialog + let promiseLoaded = BrowserTestUtils.promiseAlertDialog("cancel"); + let bookmarkedNode = + gLibrary.PlacesOrganizer._places.selectedNode.getChild(0); + mouseEventOnCell( + gLibrary.PlacesOrganizer._places, + gLibrary.PlacesOrganizer._places.view.treeIndexForNode(bookmarkedNode), + 0, + { button: 1 } + ); + + await promiseLoaded; + + Assert.ok( + true, + "Expected dialog was shown when attempting to open folder with lots of links" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_warnOnOpenLinks() { + // Generate a list of links larger than browser.tabs.maxOpenBeforeWarn + const MAX_LINKS = 16; + let children = []; + for (let i = 0; i < MAX_LINKS; i++) { + children.push({ + title: `Highlighted Target ${i}`, + url: `http://example${i}.com`, + }); + } + + // Insert the links into the tree + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children, + }); + info("Pushed test folder into the bookmarks tree"); + + gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + info("Got selection in the Library left pane"); + + // Select all the links + gLibrary.ContentTree.view.selectAll(); + + let placesContext = gLibrary.document.getElementById("placesContext"); + let promiseContextMenu = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + + // Open up the context menu and select "Open All In Tabs" (the first item in the list) + synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, { + button: 2, + type: "contextmenu", + }); + + await promiseContextMenu; + info("Context menu opened as expected"); + + let openTabs = gLibrary.document.getElementById( + "placesContext_openBookmarkLinks:tabs" + ); + let promiseLoaded = BrowserTestUtils.promiseAlertDialog("cancel"); + + placesContext.activateItem(openTabs, {}); + + await promiseLoaded; + + Assert.ok( + true, + "Expected dialog was shown when attempting to open lots of selected links" + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) { + var selection = aTree.view.selection; + selection.select(aRowIndex); + aTree.ensureRowIsVisible(aRowIndex); + var column = aTree.columns[aColumnIndex]; + + // get cell coordinates + var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text"); + + EventUtils.synthesizeMouse( + aTree.body, + rect.x, + rect.y, + aEventDetails, + gLibrary + ); +} diff --git a/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js new file mode 100644 index 0000000000..2daf822db3 --- /dev/null +++ b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js @@ -0,0 +1,75 @@ +/** + * Tests that visits across frames are correctly represented in the database. + */ + +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/places/tests/browser"; +const PAGE_URL = BASE_URL + "/framedPage.html"; +const LEFT_URL = BASE_URL + "/frameLeft.html"; +const RIGHT_URL = BASE_URL + "/frameRight.html"; + +add_task(async function test() { + // We must wait for both frames to be loaded and the visits to be registered. + let deferredLeftFrameVisit = Promise.withResolvers(); + let deferredRightFrameVisit = Promise.withResolvers(); + + Services.obs.addObserver(function observe(subject) { + (async function () { + let url = subject.QueryInterface(Ci.nsIURI).spec; + if (url == LEFT_URL) { + is( + await getTransitionForUrl(url), + null, + "Embed visits should not get a database entry." + ); + deferredLeftFrameVisit.resolve(); + } else if (url == RIGHT_URL) { + is( + await getTransitionForUrl(url), + PlacesUtils.history.TRANSITION_FRAMED_LINK, + "User activated visits should get a FRAMED_LINK transition." + ); + Services.obs.removeObserver(observe, "uri-visit-saved"); + deferredRightFrameVisit.resolve(); + } + })(); + }, "uri-visit-saved"); + + // Open a tab and wait for all the subframes to load. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL); + + // Wait for the left frame visit to be registered. + info("Waiting left frame visit"); + await deferredLeftFrameVisit.promise; + + // Click on the link in the left frame to cause a page load in the + // right frame. + info("Clicking link"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.frames[0].document.getElementById("clickme").click(); + }); + + // Wait for the right frame visit to be registered. + info("Waiting right frame visit"); + await deferredRightFrameVisit.promise; + + BrowserTestUtils.removeTab(tab); +}); + +function getTransitionForUrl(url) { + return PlacesUtils.withConnectionWrapper( + "browser_markPageAsFollowedLink", + async db => { + let rows = await db.execute( + ` + SELECT visit_type + FROM moz_historyvisits + JOIN moz_places h ON place_id = h.id + WHERE url_hash = hash(:url) AND url = :url + `, + { url } + ); + return rows.length ? rows[0].getResultByName("visit_type") : null; + } + ); +} diff --git a/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js b/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js new file mode 100644 index 0000000000..3a5527a689 --- /dev/null +++ b/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); +let gCUITestUtils = new CustomizableUITestUtils(window); + +const TEST_URL = "https://www.example.com/"; + +/** + * Checks that the Bookmarks subview is updated after deleting an item. + */ +add_task(async function test_panelview_bookmarks_delete() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: TEST_URL, + }); + + await gCUITestUtils.openMainMenu(); + + document.getElementById("appMenu-bookmarks-button").click(); + let bookmarksView = document.getElementById("PanelUI-bookmarks"); + let promise = BrowserTestUtils.waitForEvent(bookmarksView, "ViewShown"); + await promise; + + let list = document.getElementById("panelMenu_bookmarksMenu"); + let listItem = [...list.children].find(node => node.label == TEST_URL); + + let placesContext = document.getElementById("placesContext"); + promise = BrowserTestUtils.waitForEvent(placesContext, "popupshown"); + EventUtils.synthesizeMouseAtCenter(listItem, { + button: 2, + type: "contextmenu", + }); + await promise; + + promise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + if (listItem.parentNode == null) { + Assert.ok(true, "The bookmarks list item was removed."); + observer.disconnect(); + resolve(); + } + }); + observer.observe(list, { childList: true }); + }); + let placesContextDelete = document.getElementById( + "placesContext_deleteBookmark" + ); + placesContext.activateItem(placesContextDelete, {}); + await promise; + + await gCUITestUtils.hideMainMenu(); +}); diff --git a/browser/components/places/tests/browser/browser_paste_bookmarks.js b/browser/components/places/tests/browser/browser_paste_bookmarks.js new file mode 100644 index 0000000000..8a23db4cdc --- /dev/null +++ b/browser/components/places/tests/browser/browser_paste_bookmarks.js @@ -0,0 +1,439 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "http://example.com/"; +const TEST_URL1 = "https://example.com/otherbrowser/"; + +var PlacesOrganizer; +var ContentTree; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + let organizer = await promiseLibrary(); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(organizer); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + PlacesOrganizer = organizer.PlacesOrganizer; + ContentTree = organizer.ContentTree; + + // Show date added column. + await showLibraryColumn(organizer, "placesContentDateAdded"); +}); + +add_task(async function paste() { + info("Selecting BookmarksToolbar in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + let dateAdded = new Date(); + dateAdded.setHours(10); + dateAdded.setMinutes(10); + dateAdded.setSeconds(0); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URL, + title: "0", + dateAdded, + }); + + ContentTree.view.selectItems([bookmark.guid]); + + await promiseClipboard(() => { + info("Cutting selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.unfiledGuid + ); + + Assert.equal( + tree.children.length, + 1, + "Should be one bookmark in the unfiled folder." + ); + Assert.equal(tree.children[0].title, "0", "Should have the correct title"); + Assert.equal(tree.children[0].uri, TEST_URL, "Should have the correct URL"); + Assert.equal( + tree.children[0].dateAdded, + PlacesUtils.toPRTime(dateAdded), + "Should have the correct date" + ); + + Assert.ok( + ContentTree.view.view + .getCellText(0, ContentTree.view.columns.placesContentDateAdded) + .startsWith(`${dateAdded.getHours()}:${dateAdded.getMinutes()}`), + "Should reflect the data added" + ); + + await PlacesUtils.bookmarks.remove(tree.children[0].guid); +}); + +add_task(async function paste_check_indexes() { + info("Selecting BookmarksToolbar in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + let copyChildren = []; + let targetChildren = []; + for (let i = 0; i < 10; i++) { + copyChildren.push({ + url: `${TEST_URL}${i}`, + title: `Copy ${i}`, + }); + targetChildren.push({ + url: `${TEST_URL1}${i}`, + title: `Target ${i}`, + }); + } + + let copyBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: copyChildren, + }); + + let targetBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: targetChildren, + }); + + ContentTree.view.selectItems([ + copyBookmarks[0].guid, + copyBookmarks[3].guid, + copyBookmarks[6].guid, + copyBookmarks[9].guid, + ]); + + await promiseClipboard(() => { + info("Cutting multiple selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + ContentTree.view.selectItems([targetBookmarks[4].guid]); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.unfiledGuid + ); + + const expectedBookmarkOrder = [ + targetBookmarks[0].guid, + targetBookmarks[1].guid, + targetBookmarks[2].guid, + targetBookmarks[3].guid, + copyBookmarks[0].guid, + copyBookmarks[3].guid, + copyBookmarks[6].guid, + copyBookmarks[9].guid, + targetBookmarks[4].guid, + targetBookmarks[5].guid, + targetBookmarks[6].guid, + targetBookmarks[7].guid, + targetBookmarks[8].guid, + targetBookmarks[9].guid, + ]; + + Assert.equal( + tree.children.length, + expectedBookmarkOrder.length, + "Should be the expected amount of bookmarks in the unfiled folder." + ); + + for (let i = 0; i < expectedBookmarkOrder.length; ++i) { + Assert.equal( + tree.children[i].guid, + expectedBookmarkOrder[i], + `Should be the expected item at index ${i}` + ); + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function paste_check_indexes_same_folder() { + info("Selecting BookmarksToolbar in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + let copyChildren = []; + for (let i = 0; i < 10; i++) { + copyChildren.push({ + url: `${TEST_URL}${i}`, + title: `Copy ${i}`, + }); + } + + let copyBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: copyChildren, + }); + + ContentTree.view.selectItems([ + copyBookmarks[0].guid, + copyBookmarks[3].guid, + copyBookmarks[6].guid, + copyBookmarks[9].guid, + ]); + + await promiseClipboard(() => { + info("Cutting multiple selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + ContentTree.view.selectItems([copyBookmarks[4].guid]); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.toolbarGuid + ); + + // Although we've inserted at index 4, we've taken out two items below it, so + // we effectively insert after the third item. + const expectedBookmarkOrder = [ + copyBookmarks[1].guid, + copyBookmarks[2].guid, + copyBookmarks[0].guid, + copyBookmarks[3].guid, + copyBookmarks[6].guid, + copyBookmarks[9].guid, + copyBookmarks[4].guid, + copyBookmarks[5].guid, + copyBookmarks[7].guid, + copyBookmarks[8].guid, + ]; + + Assert.equal( + tree.children.length, + expectedBookmarkOrder.length, + "Should be the expected amount of bookmarks in the unfiled folder." + ); + + for (let i = 0; i < expectedBookmarkOrder.length; ++i) { + Assert.equal( + tree.children[i].guid, + expectedBookmarkOrder[i], + `Should be the expected item at index ${i}` + ); + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function paste_from_different_instance() { + let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + + // Fake data on the clipboard to pretend this is from a different instance + // of Firefox. + let data = { + title: "test", + id: 32, + instanceId: "FAKEFAKEFAKE", + itemGuid: "ZBf_TYkrYGvW", + parent: 452, + dateAdded: 1464866275853000, + lastModified: 1507638113352000, + type: "text/x-moz-place", + uri: TEST_URL1, + }; + data = JSON.stringify(data); + + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE); + xferable.setTransferData( + PlacesUtils.TYPE_X_MOZ_PLACE, + PlacesUtils.toISupportsString(data) + ); + + Services.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.unfiledGuid + ); + + Assert.equal( + tree.children.length, + 1, + "Should be one bookmark in the unfiled folder." + ); + Assert.equal(tree.children[0].title, "test", "Should have the correct title"); + Assert.equal(tree.children[0].uri, TEST_URL1, "Should have the correct URL"); + + await PlacesUtils.bookmarks.remove(tree.children[0].guid); +}); + +add_task(async function paste_separator_from_different_instance() { + let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + + // Fake data on the clipboard to pretend this is from a different instance + // of Firefox. + let data = { + title: "test", + id: 32, + instanceId: "FAKEFAKEFAKE", + itemGuid: "ZBf_TYkrYGvW", + parent: 452, + dateAdded: 1464866275853000, + lastModified: 1507638113352000, + type: PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, + }; + data = JSON.stringify(data); + + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE); + xferable.setTransferData( + PlacesUtils.TYPE_X_MOZ_PLACE, + PlacesUtils.toISupportsString(data) + ); + + Services.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.unfiledGuid + ); + + Assert.equal( + tree.children.length, + 1, + "Should be one bookmark in the unfiled folder." + ); + Assert.equal( + tree.children[0].type, + PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, + "Should have the correct type" + ); + + await PlacesUtils.bookmarks.remove(tree.children[0].guid); +}); + +add_task(async function paste_copy_check_indexes() { + info("Selecting BookmarksToolbar in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + let copyChildren = []; + let targetChildren = []; + for (let i = 0; i < 10; i++) { + copyChildren.push({ + url: `${TEST_URL}${i}`, + title: `Copy ${i}`, + }); + targetChildren.push({ + url: `${TEST_URL1}${i}`, + title: `Target ${i}`, + }); + } + + let copyBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: copyChildren, + }); + + let targetBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: targetChildren, + }); + + ContentTree.view.selectItems([ + copyBookmarks[0].guid, + copyBookmarks[3].guid, + copyBookmarks[6].guid, + copyBookmarks[9].guid, + ]); + + await promiseClipboard(() => { + info("Cutting multiple selection"); + ContentTree.view.controller.copy(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + info("Selecting UnfiledBookmarks in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + ContentTree.view.selectItems([targetBookmarks[4].guid]); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + let tree = await PlacesUtils.promiseBookmarksTree( + PlacesUtils.bookmarks.unfiledGuid + ); + + const expectedBookmarkOrder = [ + targetBookmarks[0].guid, + targetBookmarks[1].guid, + targetBookmarks[2].guid, + targetBookmarks[3].guid, + 0, + 3, + 6, + 9, + targetBookmarks[4].guid, + targetBookmarks[5].guid, + targetBookmarks[6].guid, + targetBookmarks[7].guid, + targetBookmarks[8].guid, + targetBookmarks[9].guid, + ]; + + Assert.equal( + tree.children.length, + expectedBookmarkOrder.length, + "Should be the expected amount of bookmarks in the unfiled folder." + ); + + for (let i = 0; i < expectedBookmarkOrder.length; ++i) { + if (i > 3 && i <= 7) { + // Items 4 - 7 are copies of the original, so we need to compare data, rather + // than their guids. + Assert.equal( + tree.children[i].title, + copyChildren[expectedBookmarkOrder[i]].title, + `Should have the correct bookmark title at index ${i}` + ); + Assert.equal( + tree.children[i].uri, + copyChildren[expectedBookmarkOrder[i]].url, + `Should have the correct bookmark URL at index ${i}` + ); + } else { + Assert.equal( + tree.children[i].guid, + expectedBookmarkOrder[i], + `Should be the expected item at index ${i}` + ); + } + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/places/tests/browser/browser_paste_into_tags.js b/browser/components/places/tests/browser/browser_paste_into_tags.js new file mode 100644 index 0000000000..52f312d68e --- /dev/null +++ b/browser/components/places/tests/browser/browser_paste_into_tags.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const TEST_URL = Services.io.newURI("http://example.com/"); +const MOZURISPEC = Services.io.newURI("http://mozilla.com/"); + +add_task(async function () { + let organizer = await promiseLibrary(); + + ok(PlacesUtils, "PlacesUtils in scope"); + ok(PlacesUIUtils, "PlacesUIUtils in scope"); + + let PlacesOrganizer = organizer.PlacesOrganizer; + ok(PlacesOrganizer, "Places organizer in scope"); + + let ContentTree = organizer.ContentTree; + ok(ContentTree, "ContentTree is in scope"); + + let visits = { + uri: MOZURISPEC, + transition: PlacesUtils.history.TRANSITION_TYPED, + }; + await PlacesTestUtils.addVisits(visits); + + // create an initial tag to work with + let newBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: "bookmark/" + TEST_URL.spec, + url: TEST_URL, + }); + + ok(newBookmark, "A bookmark was added"); + PlacesUtils.tagging.tagURI(TEST_URL, ["foo"]); + let tags = PlacesUtils.tagging.getTagsForURI(TEST_URL); + is(tags[0], "foo", "tag is foo"); + + // focus the new tag + focusTag(PlacesOrganizer); + + let populate = () => copyHistNode(PlacesOrganizer, ContentTree); + await promiseClipboard(populate, PlacesUtils.TYPE_X_MOZ_PLACE); + + focusTag(PlacesOrganizer); + await PlacesOrganizer._places.controller.paste(); + + // re-focus the history again + PlacesOrganizer.selectLeftPaneBuiltIn("History"); + let histContainer = PlacesOrganizer._places.selectedNode; + PlacesUtils.asContainer(histContainer); + histContainer.containerOpen = true; + PlacesOrganizer._places.selectNode(histContainer.getChild(0)); + let histNode = ContentTree.view.view.nodeForTreeIndex(0); + ok(histNode, "histNode exists: " + histNode.title); + + // check to see if the history node is tagged! + tags = PlacesUtils.tagging.getTagsForURI(MOZURISPEC); + Assert.equal(tags.length, 1, "history node is tagged: " + tags.length); + + // check if a bookmark was created + let bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: MOZURISPEC }, bm => { + bookmarks.push(bm); + }); + ok(!!bookmarks.length, "bookmark exists for the tagged history item"); + + // is the bookmark visible in the UI? + // get the Unsorted Bookmarks node + PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks"); + + // now we can see what is in the ContentTree tree + let unsortedNode = ContentTree.view.view.nodeForTreeIndex(1); + ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri); + is(unsortedNode.uri, MOZURISPEC.spec, "node uri's are the same"); + + await promiseLibraryClosed(organizer); + + // Remove new Places data we created. + PlacesUtils.tagging.untagURI(MOZURISPEC, ["foo"]); + PlacesUtils.tagging.untagURI(TEST_URL, ["foo"]); + tags = PlacesUtils.tagging.getTagsForURI(TEST_URL); + is(tags.length, 0, "tags are gone"); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +function focusTag(PlacesOrganizer) { + PlacesOrganizer.selectLeftPaneBuiltIn("Tags"); + let tags = PlacesOrganizer._places.selectedNode; + tags.containerOpen = true; + let fooTag = tags.getChild(0); + let tagNode = fooTag; + PlacesOrganizer._places.selectNode(fooTag); + is(tagNode.title, "foo", "tagNode title is foo"); + let ip = PlacesOrganizer._places.insertionPoint; + ok(ip.isTag, "IP is a tag"); +} + +function copyHistNode(PlacesOrganizer, ContentTree) { + // focus the history object + PlacesOrganizer.selectLeftPaneBuiltIn("History"); + let histContainer = PlacesOrganizer._places.selectedNode; + PlacesUtils.asContainer(histContainer); + histContainer.containerOpen = true; + PlacesOrganizer._places.selectNode(histContainer.getChild(0)); + let histNode = ContentTree.view.view.nodeForTreeIndex(0); + ContentTree.view.selectNode(histNode); + is(histNode.uri, MOZURISPEC.spec, "historyNode exists: " + histNode.uri); + // copy the history node + ContentTree.view.controller.copy(); +} diff --git a/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js b/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js new file mode 100644 index 0000000000..caa73d137f --- /dev/null +++ b/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "http://example.com/"; +const TEST_URL1 = "https://example.com/otherbrowser/"; + +var PlacesOrganizer; +var ContentTree; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + let organizer = await promiseLibrary(); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(organizer); + await PlacesUtils.bookmarks.eraseEverything(); + }); + + PlacesOrganizer = organizer.PlacesOrganizer; + ContentTree = organizer.ContentTree; +}); + +add_task(async function paste() { + info("Selecting BookmarksToolbar in the left pane"); + PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar"); + + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + url: TEST_URL, + title: "0", + }, + { + url: TEST_URL1, + title: "1", + }, + ], + }); + + Assert.equal( + ContentTree.view.view.rowCount, + 2, + "Should have the right amount of items in the view" + ); + + ContentTree.view.selectItems([bookmarks[0].guid]); + + await promiseClipboard(() => { + info("Cutting selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + assertItemsHighlighted(1); + + ContentTree.view.selectItems([bookmarks[1].guid]); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + assertItemsHighlighted(0); + + // And now repeat the other way around to make sure. + + await promiseClipboard(() => { + info("Cutting selection"); + ContentTree.view.controller.cut(); + }, PlacesUtils.TYPE_X_MOZ_PLACE); + + assertItemsHighlighted(1); + + ContentTree.view.selectItems([bookmarks[0].guid]); + + info("Pasting clipboard"); + await ContentTree.view.controller.paste(); + + assertItemsHighlighted(0); +}); + +function assertItemsHighlighted(expectedItems) { + let column = ContentTree.view.view._tree.columns[0]; + // Check the properties of the cells to make sure nothing has a cut highlight. + let highlighedItems = 0; + for (let i = 0; i < ContentTree.view.view.rowCount; i++) { + if ( + ContentTree.view.view.getCellProperties(i, column).includes("cutting") + ) { + highlighedItems++; + } + } + + Assert.equal( + highlighedItems, + expectedItems, + "Should have the correct amount of items highlighed" + ); +} diff --git a/browser/components/places/tests/browser/browser_remove_bookmarks.js b/browser/components/places/tests/browser/browser_remove_bookmarks.js new file mode 100644 index 0000000000..8889ea11c0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_remove_bookmarks.js @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test removing bookmarks from the Bookmarks Toolbar and Library. + */ + +const TEST_URL = "about:mozilla"; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_remove_bookmark_from_toolbar() { + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Bookmark Title", + url: TEST_URL, + }); + + let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid); + + let contextMenu = document.getElementById("placesContext"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + EventUtils.synthesizeMouseAtCenter(toolbarNode, { + button: 2, + type: "contextmenu", + }); + await popupShownPromise; + + let contextMenuDeleteBookmark = document.getElementById( + "placesContext_deleteBookmark" + ); + + let removePromise = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => events.some(event => event.url == TEST_URL) + ); + + contextMenu.activateItem(contextMenuDeleteBookmark, {}); + + await removePromise; + + Assert.deepEqual( + PlacesUtils.bookmarks.fetch({ url: TEST_URL }), + {}, + "Should have removed the bookmark from the database" + ); +}); + +add_task(async function test_remove_bookmark_from_library() { + const uris = [ + "http://example.com/1", + "http://example.com/2", + "http://example.com/3", + ]; + + let children = uris.map((uri, index) => { + return { + title: `bm${index}`, + url: uri, + }; + }); + + // Insert bookmarks. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children, + }); + + // Open the Library and select the "UnfiledBookmarks". + let library = await promiseLibrary("UnfiledBookmarks"); + + registerCleanupFunction(async function () { + await promiseLibraryClosed(library); + }); + + let PO = library.PlacesOrganizer; + + Assert.equal( + PlacesUtils.getConcreteItemGuid(PO._places.selectedNode), + PlacesUtils.bookmarks.unfiledGuid, + "Should have selected unfiled bookmarks." + ); + + let contextMenu = library.document.getElementById("placesContext"); + let contextMenuDeleteBookmark = library.document.getElementById( + "placesContext_deleteBookmark" + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + let firstColumn = library.ContentTree.view.columns[0]; + let firstBookmarkRect = library.ContentTree.view.getCoordsForCellItem( + 0, + firstColumn, + "bm0" + ); + + EventUtils.synthesizeMouse( + library.ContentTree.view.body, + firstBookmarkRect.x, + firstBookmarkRect.y, + { type: "contextmenu", button: 2 }, + library + ); + + await popupShownPromise; + + Assert.equal( + library.ContentTree.view.result.root.childCount, + 3, + "Number of bookmarks before removal is right" + ); + + let removePromise = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => events.some(event => event.url == uris[0]) + ); + contextMenu.activateItem(contextMenuDeleteBookmark, {}); + + await removePromise; + + Assert.equal( + library.ContentTree.view.result.root.childCount, + 2, + "Should have removed the bookmark from the display" + ); +}); diff --git a/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js b/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js new file mode 100644 index 0000000000..10f82db286 --- /dev/null +++ b/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +let bookmarks; +let folder; + +add_setup(async function () { + folder = await PlacesUtils.bookmarks.insert({ + title: "Sidebar Test Folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: folder.guid, + children: [ + { + title: "Mozilla", + url: "https://www.mozilla.org/", + }, + { + title: "Example", + url: "https://sidebar.mozilla.org/", + }, + ], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_open_multiple_bookmarks() { + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([folder.guid]); + + is( + tree.selectedNode.title, + "Sidebar Test Folder", + "The sidebar test bookmarks folder is selected" + ); + + // open all bookmarks in this folder (which is two) + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + + // expand the "Other bookmarks" folder + synthesizeClickOnSelectedTreeCell(tree, { button: 0 }); + tree.selectItems([bookmarks[0].guid]); + + is(tree.selectedNode.title, "Mozilla", "The first bookmark is selected"); + + synthesizeClickOnSelectedTreeCell(tree, { button: 0 }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "sidebar.link", + "bookmarks", + 3 + ); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + // open a bookmark in new window via context menu + synthesizeClickOnSelectedTreeCell(tree, { + button: 2, + type: "contextmenu", + }); + + let openNewWindowOption = document.getElementById( + "placesContext_open:newwindow" + ); + openNewWindowOption.click(); + + let newWin = await newWinOpened; + + // total bookmarks opened + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "bookmarks", + 4 + ); + + Services.telemetry.clearScalars(); + BrowserTestUtils.closeWindow(newWin); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + }); +}); + +add_task(async function test_bookmarks_search() { + let cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram( + "PLACES_BOOKMARKS_SEARCHBAR_CUMULATIVE_SEARCHES" + ); + cumulativeSearchesHistogram.clear(); + + await withSidebarTree("bookmarks", async tree => { + // Search the tree. + let searchBox = tree.ownerDocument.getElementById("search-box"); + searchBox.value = "example"; + searchBox.doCommand(); + + searchBox.value = ""; + searchBox.doCommand(); + info("Search was reset"); + + // Perform a second search. + searchBox.value = "mozilla"; + searchBox.doCommand(); + info("Second search was performed"); + + // Select the first link and click on it. + tree.selectNode(tree.view.nodeForTreeIndex(0)); + + synthesizeClickOnSelectedTreeCell(tree, { button: 0 }); + info("First link was selected and then clicked on"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "bookmarks", + 1 + ); + }); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1); + info("Cumulative search probe is recorded"); + + cumulativeSearchesHistogram.clear(); + Services.telemetry.clearScalars(); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js b/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js new file mode 100644 index 0000000000..cb27bf66ef --- /dev/null +++ b/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js @@ -0,0 +1,285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const firstNodeIndex = 0; + +// The prompt returns 1 for cancelled and 0 for accepted. +let gResponse = 1; +(function replacePromptService() { + let originalPromptService = Services.prompt; + Services.prompt = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]), + confirmEx: () => gResponse, + }; + registerCleanupFunction(() => { + Services.prompt = originalPromptService; + }); +})(); + +add_setup(async function () { + await PlacesUtils.history.clear(); + + // Visited pages listed by descending visit date. + let pages = [ + "https://sidebar.mozilla.org/a", + "https://sidebar.mozilla.org/b", + "https://sidebar.mozilla.org/c", + "https://www.mozilla.org/d", + ]; + + // Add some visited page. + let time = Date.now(); + let places = []; + for (let i = 0; i < pages.length; i++) { + places.push({ + uri: NetUtil.newURI(pages[i]), + visitDate: (time - i) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(places); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_click_multiple_history_entries() { + await withSidebarTree("history", async tree => { + tree.ownerDocument.getElementById("byday").doCommand(); + + // Select the first link and click on it. + tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex)); + is( + tree.selectedNode.title, + "Today", + "The Today history sidebar button is selected" + ); + + // if a user tries to open all history items and they cancel opening + // those items in new tabs (due to max tab limit warning), + // no telemetry should be recorded + gResponse = 1; + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.maxOpenBeforeWarn", 4]], + }); + + // open multiple history items with a single click + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link" + ); + + // if they proceed with opening history multiple history items despite the warning, + // telemetry should be recorded + gResponse = 0; + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 4 + ); + + let parentNode = tree.selectedNode; + if (!parentNode.containerOpen) { + // Only need to open/expand container node on first run + synthesizeClickOnSelectedTreeCell(tree); + } + if (parentNode.title == "Today" && parentNode.hasChildren) { + info(`Selecting node with title ${parentNode?.getChild(0)?.title}`); + tree.selectNode(parentNode.getChild(0)); + } + + synthesizeClickOnSelectedTreeCell(tree, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link" + ); + + let openNewTabOption = document.getElementById("placesContext_open:newtab"); + openNewTabOption.click(); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 1 + ); + + if (parentNode.title == "Today" && parentNode.hasChildren) { + tree.selectNode(parentNode.getChild(0)); + } + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + synthesizeClickOnSelectedTreeCell(tree, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link" + ); + + let openNewWindowOption = document.getElementById( + "placesContext_open:newwindow" + ); + openNewWindowOption.click(); + + let newWin = await newWinOpened; + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 1 + ); + + await BrowserTestUtils.closeWindow(newWin); + + if (parentNode.title == "Today" && parentNode.hasChildren) { + tree.selectNode(parentNode.getChild(0)); + } + + let newPrivateWinOpened = BrowserTestUtils.waitForNewWindow(); + + synthesizeClickOnSelectedTreeCell(tree, { + button: 2, + type: "contextmenu", + }); + + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link" + ); + + let openNewPrivateWindowOption = document.getElementById( + "placesContext_open:newprivatewindow" + ); + openNewPrivateWindowOption.click(); + + let newPrivateWin = await newPrivateWinOpened; + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 1 + ); + + await BrowserTestUtils.closeWindow(newPrivateWin); + }); + + await SpecialPowers.popPrefEnv(); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); + +add_task(async function test_search_and_filter() { + let cumulativeSearchesHistogram = Services.telemetry.getHistogramById( + "PLACES_SEARCHBAR_CUMULATIVE_SEARCHES" + ); + cumulativeSearchesHistogram.clear(); + let cumulativeFilterCountHistogram = Services.telemetry.getHistogramById( + "PLACES_SEARCHBAR_CUMULATIVE_FILTER_COUNT" + ); + cumulativeFilterCountHistogram.clear(); + + await withSidebarTree("history", async tree => { + // Apply a search filter. + tree.ownerDocument.getElementById("bylastvisited").doCommand(); + info("Search filter was changed to bylastvisited"); + + // Search the tree. + let searchBox = tree.ownerDocument.getElementById("search-box"); + searchBox.value = "sidebar.mozilla"; + searchBox.doCommand(); + info("Tree was searched with sting sidebar.mozilla"); + + searchBox.value = ""; + searchBox.doCommand(); + info("Search was reset"); + + // Perform a second search. + searchBox.value = "sidebar.mozilla"; + searchBox.doCommand(); + info("Second search was performed"); + + // Select the first link and click on it. + tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex)); + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + info("First link was selected and then clicked on"); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 1 + ); + }); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1); + info("Cumulative search telemetry looks right"); + + TelemetryTestUtils.assertHistogram(cumulativeFilterCountHistogram, 1, 1); + info("Cumulative search filter telemetry looks right"); + + cumulativeSearchesHistogram.clear(); + cumulativeFilterCountHistogram.clear(); + + await withSidebarTree("history", async tree => { + // Apply a search filter. + tree.ownerDocument.getElementById("byday").doCommand(); + info("First search filter applied"); + + // Apply another search filter. + tree.ownerDocument.getElementById("bylastvisited").doCommand(); + info("Second search filter applied"); + + // Apply a search filter. + tree.ownerDocument.getElementById("byday").doCommand(); + info("Third search filter applied"); + + // Search the tree. + let searchBox = tree.ownerDocument.getElementById("search-box"); + searchBox.value = "sidebar.mozilla"; + searchBox.doCommand(); + + // Select the first link and click on it. + tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex)); + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "sidebar.link", + "history", + 1 + ); + }); + + TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(cumulativeFilterCountHistogram, 3, 1); + + cumulativeSearchesHistogram.clear(); + cumulativeFilterCountHistogram.clear(); + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/places/tests/browser/browser_sidebar_on_customization.js b/browser/components/places/tests/browser/browser_sidebar_on_customization.js new file mode 100644 index 0000000000..6e97f81dd3 --- /dev/null +++ b/browser/components/places/tests/browser/browser_sidebar_on_customization.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var gInsertedBookmarks; +add_setup(async function () { + gInsertedBookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "about:buildconfig", + }, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "folder", + children: [ + { + title: "bm2", + url: "about:mozilla", + }, + ], + }, + ], + }); + registerCleanupFunction(PlacesUtils.bookmarks.eraseEverything); +}); + +add_task(async function test_open_sidebar_and_customize() { + await withSidebarTree("bookmarks", async tree => { + async function checkTreeIsFunctional() { + Assert.ok(SidebarUI.isOpen, "Sidebar is open"); + Assert.ok( + BrowserTestUtils.isVisible(SidebarUI.browser), + "sidebar browser is visible" + ); + Assert.ok(tree.view.result, "View result is defined"); + await TestUtils.waitForCondition( + () => tree.view.result.root.containerOpen, + "View root node should be reopened" + ); + toggleFolder(tree, gInsertedBookmarks[1].guid); + } + + await checkTreeIsFunctional(); + + info("Starting customization"); + await promiseCustomizeStart(); + + Assert.ok( + !BrowserTestUtils.isVisible(SidebarUI.browser), + "sidebar browser is hidden" + ); + Assert.ok(tree.view.result, "View result is defined"); + Assert.ok(!tree.view.result.root.containerOpen, "View root node is closed"); + + info("Ending customization"); + await promiseCustomizeEnd(); + + await checkTreeIsFunctional(); + }); +}); + +function promiseCustomizeStart(win = window) { + return new Promise(resolve => { + win.gNavToolbox.addEventListener("customizationready", resolve, { + once: true, + }); + win.gCustomizeMode.enter(); + }); +} + +function promiseCustomizeEnd(win = window) { + return new Promise(resolve => { + win.gNavToolbox.addEventListener("aftercustomization", resolve, { + once: true, + }); + win.gCustomizeMode.exit(); + }); +} + +function toggleFolder(tree, guid) { + tree.selectItems([guid]); + Assert.equal(tree.selectedNode.title, "folder"); + Assert.ok( + !PlacesUtils.asContainer(tree.selectedNode).containerOpen, + "Folder is closed" + ); + synthesizeClickOnSelectedTreeCell(tree); + Assert.ok( + PlacesUtils.asContainer(tree.selectedNode).containerOpen, + "Folder is open" + ); + synthesizeClickOnSelectedTreeCell(tree); + Assert.ok( + !PlacesUtils.asContainer(tree.selectedNode).containerOpen, + "Folder is closed" + ); +} diff --git a/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js b/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js new file mode 100644 index 0000000000..92f98b898c --- /dev/null +++ b/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs"; + +var gBms; + +add_setup(async function () { + gBms = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bm1", + url: "about:buildconfig", + }, + { + title: "bm2", + url: "about:mozilla", + }, + ], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_open_bookmark_from_sidebar() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([gBms[0].guid]); + + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + gBms[0].url + ); + + tree.controller.doCommand("placesCmd_open"); + + await loadedPromise; + + // An assert to make the test happy. + Assert.ok(true, "The bookmark was loaded successfully."); + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_open_bookmark_from_sidebar_keypress() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([gBms[1].guid]); + + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + gBms[1].url + ); + + tree.focus(); + EventUtils.sendKey("return"); + + await loadedPromise; + + // An assert to make the test happy. + Assert.ok(true, "The bookmark was loaded successfully."); + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_open_bookmark_in_tab_from_sidebar() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]], + }); + + await BrowserTestUtils.withNewTab({ gBrowser }, async initialTab => { + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([gBms[0].guid]); + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + gBms[0].url + ); + tree.focus(); + EventUtils.sendKey("return"); + await loadedPromise; + Assert.ok(true, "The bookmark reused the empty tab."); + + tree.selectItems([gBms[1].guid]); + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, gBms[1].url); + tree.focus(); + EventUtils.sendKey("return"); + let newTab = await newTabPromise; + Assert.ok(true, "The bookmark was opened in a new tab."); + BrowserTestUtils.removeTab(newTab); + }); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_open_bookmark_folder_from_sidebar() { + await withSidebarTree("bookmarks", async tree => { + tree.selectItems([PlacesUtils.bookmarks.virtualUnfiledGuid]); + + Assert.equal( + tree.view.selection.getRangeCount(), + 1, + "Should only have one range selected" + ); + + let loadedPromises = []; + + for (let bm of gBms) { + loadedPromises.push( + BrowserTestUtils.waitForNewTab(gBrowser, bm.url, false, true) + ); + } + + synthesizeClickOnSelectedTreeCell(tree, { button: 1 }); + + let tabs = await Promise.all(loadedPromises); + + for (let tab of tabs) { + await BrowserTestUtils.removeTab(tab); + } + }); +}); diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js new file mode 100644 index 0000000000..4b231c92b0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure that the items in the bookmarks and history sidebar +// panels are clickable in both LTR and RTL modes. + +var sidebar; + +function pushPref(name, val) { + return SpecialPowers.pushPrefEnv({ set: [[name, val]] }); +} + +function popPref() { + return SpecialPowers.popPrefEnv(); +} + +add_task(async function test_sidebarpanels_click() { + ignoreAllUncaughtExceptions(); + + const BOOKMARKS_SIDEBAR_ID = "viewBookmarksSidebar"; + const BOOKMARKS_SIDEBAR_TREE_ID = "bookmarks-view"; + const HISTORY_SIDEBAR_ID = "viewHistorySidebar"; + const HISTORY_SIDEBAR_TREE_ID = "historyTree"; + const TEST_URL = + "http://mochi.test:8888/browser/browser/components/places/tests/browser/sidebarpanels_click_test_page.html"; + + // If a sidebar is already open, close it. + if (!document.getElementById("sidebar-box").hidden) { + ok( + false, + "Unexpected sidebar found - a previous test failed to cleanup correctly" + ); + SidebarUI.hide(); + } + + // Ensure history is clean before starting the test. + await PlacesUtils.history.clear(); + + sidebar = document.getElementById("sidebar"); + let tests = []; + + tests.push({ + _bookmark: null, + async init() { + // Add a bookmark to the Unfiled Bookmarks folder. + this._bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test", + url: TEST_URL, + }); + }, + prepare() {}, + async selectNode(tree) { + tree.selectItems([this._bookmark.guid]); + }, + cleanup(aCallback) { + return PlacesUtils.bookmarks.remove(this._bookmark); + }, + sidebarName: BOOKMARKS_SIDEBAR_ID, + treeName: BOOKMARKS_SIDEBAR_TREE_ID, + desc: "Bookmarks sidebar test", + }); + + tests.push({ + async init() { + // Add a history entry. + let uri = Services.io.newURI(TEST_URL); + await PlacesTestUtils.addVisits({ + uri, + visitDate: Date.now() * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + }, + prepare() { + sidebar.contentDocument.getElementById("byvisited").doCommand(); + }, + selectNode(tree) { + tree.selectNode(tree.view.nodeForTreeIndex(0)); + is( + tree.selectedNode.uri, + TEST_URL, + "The correct visit has been selected" + ); + is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked"); + }, + cleanup(aCallback) { + return PlacesUtils.history.clear(); + }, + sidebarName: HISTORY_SIDEBAR_ID, + treeName: HISTORY_SIDEBAR_TREE_ID, + desc: "History sidebar test", + }); + + for (let test of tests) { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + info("Running " + test.desc + " in LTR mode"); + await testPlacesPanel(test); + + await pushPref("intl.l10n.pseudo", "bidi"); + info("Running " + test.desc + " in RTL mode"); + await testPlacesPanel(test); + await popPref(); + + // Remove tabs created by sub-tests. + while (gBrowser.tabs.length > 1) { + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + } + } +}); + +async function testPlacesPanel(testInfo) { + await testInfo.init(); + + let promise = new Promise(resolve => { + sidebar.addEventListener( + "load", + function () { + executeSoon(async function () { + testInfo.prepare(); + + let tree = sidebar.contentDocument.getElementById(testInfo.treeName); + + // Select the inserted places item. + await testInfo.selectNode(tree); + + let promiseAlert = promiseAlertDialogObserved(); + + synthesizeClickOnSelectedTreeCell(tree); + // Now, wait for the observer to catch the alert dialog. + // If something goes wrong, the test will time out at this stage. + // Note that for the history sidebar, the URL itself is not opened, + // and Places will show the load-js-data-url-error prompt as an alert + // box, which means that the click actually worked, so it's good enough + // for the purpose of this test. + + await promiseAlert; + + executeSoon(async function () { + SidebarUI.hide(); + await testInfo.cleanup(); + resolve(); + }); + }); + }, + { capture: true, once: true } + ); + }); + + SidebarUI.show(testInfo.sidebarName); + + return promise; +} + +function promiseAlertDialogObserved() { + return new Promise(resolve => { + async function observer(subject) { + info("alert dialog observed as expected"); + Services.obs.removeObserver(observer, "common-dialog-loaded"); + Services.obs.removeObserver(observer, "tabmodal-dialog-loaded"); + + if (subject.Dialog) { + subject.Dialog.ui.button0.click(); + } else { + subject.querySelector(".tabmodalprompt-button0").click(); + } + resolve(); + } + Services.obs.addObserver(observer, "common-dialog-loaded"); + Services.obs.addObserver(observer, "tabmodal-dialog-loaded"); + }); +} diff --git a/browser/components/places/tests/browser/browser_sort_in_library.js b/browser/components/places/tests/browser/browser_sort_in_library.js new file mode 100644 index 0000000000..2eea3a3f31 --- /dev/null +++ b/browser/components/places/tests/browser/browser_sort_in_library.js @@ -0,0 +1,248 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the following bugs: + * + * Bug 443745 - View>Sort>of "alpha" sort items is default to Z>A instead of A>Z + * https://bugzilla.mozilla.org/show_bug.cgi?id=443745 + * + * Bug 444179 - Library>Views>Sort>Sort by Tags does nothing + * https://bugzilla.mozilla.org/show_bug.cgi?id=444179 + * + * Basically, fully tests sorting the placeContent tree in the Places Library + * window. Sorting is verified by comparing the nsINavHistoryResult returned by + * placeContent.result to the expected sort values. + */ + +// Two properties of nsINavHistoryResult control the sort of the tree: +// sortingMode. sortingMode's value is one of the +// nsINavHistoryQueryOptions.SORT_BY_* constants. +// +// This lookup table maps the possible values of anonid's of the treecols to +// objects that represent the treecols' correct state after the user sorts the +// previously unsorted tree by selecting a column from the Views > Sort menu. +// sortingMode is constructed from the key and dir properties (i.e., +// SORT_BY_<key>_<dir>). +const SORT_LOOKUP_TABLE = { + title: { key: "TITLE", dir: "ASCENDING" }, + tags: { key: "TAGS", dir: "ASCENDING" }, + url: { key: "URI", dir: "ASCENDING" }, + date: { key: "DATE", dir: "DESCENDING" }, + visitCount: { key: "VISITCOUNT", dir: "DESCENDING" }, + dateAdded: { key: "DATEADDED", dir: "DESCENDING" }, + lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" }, +}; + +// This is the column that's sorted if one is not specified and the tree is +// currently unsorted. Set it to a key substring in the name of one of the +// nsINavHistoryQueryOptions.SORT_BY_* constants, e.g., "TITLE", "URI". +// Method ViewMenu.setSortColumn in browser/components/places/content/places.js +// determines this value. +const DEFAULT_SORT_KEY = "TITLE"; + +// Part of the test is checking that sorts stick, so each time we sort we need +// to remember it. +var prevSortDir = null; +var prevSortKey = null; + +/** + * Ensures that the sort of aTree is aSortingMode + * + * @param {object} aTree + * the tree to check + * @param {Ci.nsINavHistoryQueryOptions} aSortingMode + * one of the Ci.nsINavHistoryQueryOptions.SORT_BY_* constants + */ +function checkSort(aTree, aSortingMode) { + // The placeContent tree's sort is determined by the nsINavHistoryResult it + // stores. Get it and check that the sort is what the caller expects. + let res = aTree.result; + isnot(res, null, "sanity check: placeContent.result should not return null"); + + // Check sortingMode. + is( + res.sortingMode, + aSortingMode, + "column should now have sortingMode " + aSortingMode + ); +} + +/** + * Sets the sort of aTree. + * + * @param {object} aOrganizerWin + * the Places window + * @param {object} aTree + * the tree to sort + * @param {boolean} aUnsortFirst + * true if the sort should be set to SORT_BY_NONE before sorting by aCol + * and aDir + * @param {boolean} aShouldFail + * true if setSortColumn should fail on aCol or aDir + * @param {object} aCol + * the column of aTree by which to sort + * @param {string} aDir + * either "ascending" or "descending" + */ +function setSort(aOrganizerWin, aTree, aUnsortFirst, aShouldFail, aCol, aDir) { + if (aUnsortFirst) { + aOrganizerWin.ViewMenu.setSortColumn(); + checkSort(aTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, ""); + + // Remember the sort key and direction. + prevSortKey = null; + prevSortDir = null; + } + + let failed = false; + try { + aOrganizerWin.ViewMenu.setSortColumn(aCol, aDir); + + // Remember the sort key and direction. + if (!aCol && !aDir) { + prevSortKey = null; + prevSortDir = null; + } else { + if (aCol) { + prevSortKey = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].key; + } else if (prevSortKey === null) { + prevSortKey = DEFAULT_SORT_KEY; + } + + if (aDir) { + prevSortDir = aDir.toUpperCase(); + } else if (prevSortDir === null) { + prevSortDir = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].dir; + } + } + } catch (exc) { + failed = true; + } + + is( + failed, + !!aShouldFail, + "setSortColumn on column " + + (aCol ? aCol.getAttribute("anonid") : "(no column)") + + " with direction " + + (aDir || "(no direction)") + + " and table previously " + + (aUnsortFirst ? "unsorted" : "sorted") + + " should " + + (aShouldFail ? "" : "not ") + + "fail" + ); +} + +/** + * Tries sorting by an invalid column and sort direction. + * + * @param {object} aOrganizerWin + * the Places window + * @param {object} aPlaceContentTree + * the placeContent tree in aOrganizerWin + */ +function testInvalid(aOrganizerWin, aPlaceContentTree) { + // Invalid column should fail by throwing an exception. + let bogusCol = document.createXULElement("treecol"); + bogusCol.setAttribute("anonid", "bogusColumn"); + setSort(aOrganizerWin, aPlaceContentTree, true, true, bogusCol, "ascending"); + + // Invalid direction reverts to SORT_BY_NONE. + setSort(aOrganizerWin, aPlaceContentTree, false, false, null, "bogus dir"); + checkSort(aPlaceContentTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, ""); +} + +/** + * Tests sorting aPlaceContentTree by column only and then by both column + * and direction. + * + * @param {object} aOrganizerWin + * the Places window + * @param {object} aPlaceContentTree + * the placeContent tree in aOrganizerWin + * @param {boolean} aUnsortFirst + * true if, before each sort we try, we should sort to SORT_BY_NONE + */ +function testSortByColAndDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) { + let cols = aPlaceContentTree.getElementsByTagName("treecol"); + ok(!!cols.length, "sanity check: placeContent should contain columns"); + + for (let i = 0; i < cols.length; i++) { + let col = cols.item(i); + ok( + col.hasAttribute("anonid"), + "sanity check: column " + col.id + " should have anonid" + ); + + let colId = col.getAttribute("anonid"); + ok( + colId in SORT_LOOKUP_TABLE, + "sanity check: unexpected placeContent column anonid" + ); + + let sortStr = + "SORT_BY_" + + SORT_LOOKUP_TABLE[colId].key + + "_" + + (aUnsortFirst ? SORT_LOOKUP_TABLE[colId].dir : prevSortDir); + let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr]; + + // Test sorting by only a column. + setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col); + checkSort(aPlaceContentTree, expectedSortMode); + + // Test sorting by both a column and a direction. + ["ascending", "descending"].forEach(function (dir) { + sortStr = + "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" + dir.toUpperCase(); + expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr]; + setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col, dir); + checkSort(aPlaceContentTree, expectedSortMode); + }); + } +} + +/** + * Tests sorting aPlaceContentTree by direction only. + * + * @param {object} aOrganizerWin + * the Places window + * @param {object} aPlaceContentTree + * the placeContent tree in aOrganizerWin + * @param {boolean} aUnsortFirst + * true if, before each sort we try, we should sort to SORT_BY_NONE + */ +function testSortByDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) { + ["ascending", "descending"].forEach(function (dir) { + let key = aUnsortFirst ? DEFAULT_SORT_KEY : prevSortKey; + let sortStr = "SORT_BY_" + key + "_" + dir.toUpperCase(); + let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr]; + setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, null, dir); + checkSort(aPlaceContentTree, expectedSortMode, ""); + }); +} + +function test() { + waitForExplicitFinish(); + + openLibrary(function (win) { + let tree = win.document.getElementById("placeContent"); + isnot(tree, null, "sanity check: placeContent tree should exist"); + // Run the tests. + testSortByColAndDir(win, tree, true); + testSortByColAndDir(win, tree, false); + testSortByDir(win, tree, true); + testSortByDir(win, tree, false); + testInvalid(win, tree); + // Reset the sort to SORT_BY_NONE. + setSort(win, tree, false, false); + // Close the window and finish. + win.close(); + finish(); + }); +} diff --git a/browser/components/places/tests/browser/browser_stayopenmenu.js b/browser/components/places/tests/browser/browser_stayopenmenu.js new file mode 100644 index 0000000000..ec26700ef7 --- /dev/null +++ b/browser/components/places/tests/browser/browser_stayopenmenu.js @@ -0,0 +1,267 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Menus should stay open (if pref is set) after ctrl-click, middle-click, +// and contextmenu's "Open in a new tab" click. + +async function locateBookmarkAndTestCtrlClick(menupopup) { + let testMenuitem = [...menupopup.children].find( + node => node.label == "Test1" + ); + ok(testMenuitem, "Found test bookmark."); + ok(BrowserTestUtils.isVisible(testMenuitem), "Should be visible"); + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(testMenuitem, { accelKey: true }); + let newTab = await promiseTabOpened; + ok(true, "Bookmark ctrl-click opened new tab."); + BrowserTestUtils.removeTab(newTab); + return testMenuitem; +} + +async function testContextmenu(menuitem) { + let doc = menuitem.ownerDocument; + let cm = doc.getElementById("placesContext"); + let promiseEvent = BrowserTestUtils.waitForEvent(cm, "popupshown"); + EventUtils.synthesizeMouseAtCenter(menuitem, { + type: "contextmenu", + button: 2, + }); + await promiseEvent; + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + let hidden = BrowserTestUtils.waitForEvent(cm, "popuphidden"); + cm.activateItem(doc.getElementById("placesContext_open:newtab")); + await hidden; + let newTab = await promiseTabOpened; + return newTab; +} + +add_setup(async function () { + // Ensure BMB is available in UI. + let origBMBlocation = CustomizableUI.getPlacementOfWidget( + "bookmarks-menu-button" + ); + if (!origBMBlocation) { + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR + ); + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.bookmarks.openInTabClosesMenu", false]], + }); + // Ensure menubar visible. + let menubar = document.getElementById("toolbar-menubar"); + let menubarVisible = isToolbarVisible(menubar); + if (!menubarVisible) { + setToolbarVisibility(menubar, true); + info("Menubar made visible"); + } + // Ensure Bookmarks Toolbar Visible. + let toolbar = document.getElementById("PersonalToolbar"); + let toolbarHidden = toolbar.collapsed; + if (toolbarHidden) { + await promiseSetToolbarVisibility(toolbar, true); + info("Bookmarks toolbar made visible"); + } + // Create our test bookmarks. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://example.com/", + title: "Test1", + }); + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "TEST_TITLE", + index: 0, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://example.com/", + title: "Test1", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + // if BMB was not originally in UI, remove it. + if (!origBMBlocation) { + CustomizableUI.removeWidgetFromArea("bookmarks-menu-button"); + } + // Restore menubar to original visibility. + setToolbarVisibility(menubar, menubarVisible); + // Restore original bookmarks toolbar visibility. + if (toolbarHidden) { + await promiseSetToolbarVisibility(toolbar, false); + } + }); +}); + +add_task(async function testStayopenBookmarksClicks() { + // Test Bookmarks Menu Button stayopen clicks - Ctrl-click. + let BMB = document.getElementById("bookmarks-menu-button"); + let BMBpopup = document.getElementById("BMB_bookmarksPopup"); + let promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(BMB, {}); + await promiseEvent; + info("Popupshown on Bookmarks-Menu-Button"); + var menuitem = await locateBookmarkAndTestCtrlClick(BMBpopup); + ok(BMB.open, "Bookmarks Menu Button's Popup should still be open."); + + // Test Bookmarks Menu Button stayopen clicks: middle-click. + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 }); + let newTab = await promiseTabOpened; + ok(true, "Bookmark middle-click opened new tab."); + BrowserTestUtils.removeTab(newTab); + ok(BMB.open, "Bookmarks Menu Button's Popup should still be open."); + + // Test Bookmarks Menu Button stayopen clicks - 'Open in new tab' on context menu. + newTab = await testContextmenu(menuitem); + ok(true, "Bookmark contextmenu opened new tab."); + ok(BMB.open, "Bookmarks Menu Button's Popup should still be open."); + promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popuphidden"); + BMB.open = false; + await promiseEvent; + info("Closing menu"); + BrowserTestUtils.removeTab(newTab); + + // Test App Menu's Bookmarks Library stayopen clicks. + let appMenu = document.getElementById("PanelUI-menu-button"); + let appMenuPopup = document.getElementById("appMenu-popup"); + let PopupShownPromise = BrowserTestUtils.waitForEvent( + appMenuPopup, + "popupshown" + ); + appMenu.click(); + await PopupShownPromise; + + let BMview; + document.getElementById("appMenu-bookmarks-button").click(); + BMview = document.getElementById("PanelUI-bookmarks"); + let promise = BrowserTestUtils.waitForEvent(BMview, "ViewShown"); + await promise; + info("Bookmarks panel shown."); + + // Test App Menu's Bookmarks Library stayopen clicks: Ctrl-click. + let menu = document.getElementById("panelMenu_bookmarksMenu"); + var testMenuitem = await locateBookmarkAndTestCtrlClick(menu); + ok(appMenu.open, "Menu should remain open."); + + // Test App Menu's Bookmarks Library stayopen clicks: middle-click. + promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(testMenuitem, { button: 1 }); + newTab = await promiseTabOpened; + ok(true, "Bookmark middle-click opened new tab."); + BrowserTestUtils.removeTab(newTab); + ok( + PanelView.forNode(BMview).active, + "Should still show the bookmarks subview" + ); + ok(appMenu.open, "Menu should remain open."); + + // Close the App Menu + appMenuPopup.hidePopup(); + ok(!appMenu.open, "The menu should now be closed."); + + // Disable the rest of the tests on Mac due to Mac's handling of menus being + // slightly different to the other platforms. + if (AppConstants.platform === "macosx") { + return; + } + + // Test Bookmarks Menu (menubar) stayopen clicks: Ctrl-click. + let BM = document.getElementById("bookmarksMenu"); + let BMpopup = document.getElementById("bookmarksMenuPopup"); + promiseEvent = BrowserTestUtils.waitForEvent(BMpopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(BM, {}); + await promiseEvent; + info("Popupshowing on Bookmarks Menu"); + menuitem = await locateBookmarkAndTestCtrlClick(BMpopup); + ok(BM.open, "Bookmarks Menu's Popup should still be open."); + + // Test Bookmarks Menu (menubar) stayopen clicks: middle-click. + promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 }); + newTab = await promiseTabOpened; + ok(true, "Bookmark middle-click opened new tab."); + BrowserTestUtils.removeTab(newTab); + ok(BM.open, "Bookmarks Menu's Popup should still be open."); + + // Test Bookmarks Menu (menubar) stayopen clicks: 'Open in new tab' on context menu. + newTab = await testContextmenu(menuitem); + ok(true, "Bookmark contextmenu opened new tab."); + BrowserTestUtils.removeTab(newTab); + ok(BM.open, "Bookmarks Menu's Popup should still be open."); + promiseEvent = BrowserTestUtils.waitForEvent(BMpopup, "popuphidden"); + BM.open = false; + await promiseEvent; + + // Test Bookmarks Toolbar stayopen clicks - Ctrl-click. + let BT = document.getElementById("PlacesToolbarItems"); + let toolbarbutton = BT.firstElementChild; + ok(toolbarbutton, "Folder should be first item on Bookmarks Toolbar."); + let buttonMenupopup = toolbarbutton.firstElementChild; + Assert.equal( + buttonMenupopup.tagName, + "menupopup", + "Found toolbar button's menupopup." + ); + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(toolbarbutton, {}); + await promiseEvent; + ok(true, "Bookmarks toolbar folder's popup is open."); + menuitem = buttonMenupopup.firstElementChild.nextElementSibling; + promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(menuitem, { ctrlKey: true }); + newTab = await promiseTabOpened; + ok( + true, + "Bookmark in folder on bookmark's toolbar ctrl-click opened new tab." + ); + ok( + toolbarbutton.open, + "Popup of folder on bookmark's toolbar should still be open." + ); + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden"); + toolbarbutton.open = false; + await promiseEvent; + BrowserTestUtils.removeTab(newTab); + + // Test Bookmarks Toolbar stayopen clicks: middle-click. + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(toolbarbutton, {}); + await promiseEvent; + ok(true, "Bookmarks toolbar folder's popup is open."); + promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null); + EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 }); + newTab = await promiseTabOpened; + ok( + true, + "Bookmark in folder on Bookmarks Toolbar middle-click opened new tab." + ); + ok( + toolbarbutton.open, + "Popup of folder on bookmark's toolbar should still be open." + ); + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden"); + toolbarbutton.open = false; + await promiseEvent; + BrowserTestUtils.removeTab(newTab); + + // Test Bookmarks Toolbar stayopen clicks: 'Open in new tab' on context menu. + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(toolbarbutton, {}); + await promiseEvent; + ok(true, "Bookmarks toolbar folder's popup is open."); + newTab = await testContextmenu(menuitem); + ok(true, "Bookmark on Bookmarks Toolbar contextmenu opened new tab."); + ok( + toolbarbutton.open, + "Popup of folder on bookmark's toolbar should still be open." + ); + promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden"); + toolbarbutton.open = false; + await promiseEvent; + BrowserTestUtils.removeTab(newTab); +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js b/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js new file mode 100644 index 0000000000..9815bd595d --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const sandbox = sinon.createSandbox(); + +const URL1 = "https://example.com/1/"; +const URL2 = "https://example.com/2/"; +const BOOKMARKLET_URL = `javascript: (() => {alert('Hello, World!');})();`; +let bookmarks; + +registerCleanupFunction(async function () { + sandbox.restore(); +}); + +add_task(async function test() { + // Make sure the bookmarks bar is visible and restore its state on cleanup. + let toolbar = document.getElementById("PersonalToolbar"); + ok(toolbar, "PersonalToolbar should not be null"); + + await PlacesUtils.bookmarks.eraseEverything(); + + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + // Setup the node we will use to be dropped. The actual node used does not + // matter because we will set its data, effect, and mimeType manually. + let placesItems = document.getElementById("PlacesToolbarItems"); + Assert.ok(placesItems, "PlacesToolbarItems should not be null"); + + /** + * Simulates a drop of a bookmarklet URI onto the bookmarks bar. + * + * @param {string} aEffect + * The effect to use for the drop operation: move, copy, or link. + */ + let simulateDragDrop = async function (aEffect) { + info("Simulates drag/drop of a new javascript:URL to the bookmarks"); + await withBookmarksDialog( + true, + function openDialog() { + EventUtils.synthesizeDrop( + toolbar, + placesItems, + [[{ type: "text/x-moz-url", data: BOOKMARKLET_URL }]], + aEffect, + window + ); + }, + async function testNameField(dialogWin) { + info("Checks that there is a javascript:URL in ShowBookmarksDialog"); + + let location = dialogWin.document.getElementById( + "editBMPanel_locationField" + ).value; + + Assert.equal( + location, + BOOKMARKLET_URL, + "Should have opened the ShowBookmarksDialog with the correct bookmarklet url to be bookmarked" + ); + } + ); + + info("Simulates drag/drop of a new URL to the bookmarks"); + let spy = sandbox + .stub(PlacesUIUtils, "showBookmarkDialog") + .returns(Promise.resolve()); + + let promise = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => url == URL1) + ); + + EventUtils.synthesizeDrop( + toolbar, + placesItems, + [[{ type: "text/x-moz-url", data: URL1 }]], + aEffect, + window + ); + + await promise; + Assert.ok(spy.notCalled, "ShowBookmarksDialog on drop not called for url"); + sandbox.restore(); + }; + + let effects = ["copy", "link"]; + for (let effect of effects) { + await simulateDragDrop(effect); + } + + info("Move of existing bookmark / bookmarklet on toolbar"); + // Clean previous bookmarks to ensure right ids count. + await PlacesUtils.bookmarks.eraseEverything(); + + info("Insert list of bookamrks to have bookmarks (ids) for moving"); + // Ensure bookmarks are visible on the toolbar. + let promiseBookmarksOnToolbar = BrowserTestUtils.waitForMutationCondition( + placesItems, + { childList: true }, + () => placesItems.childNodes.length == 3 + ); + bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + title: "bm1", + url: URL1, + }, + { + title: "bm2", + url: URL2, + }, + { + title: "bookmarklet", + url: BOOKMARKLET_URL, + }, + ], + }); + await promiseBookmarksOnToolbar; + + let spy = sandbox + .stub(PlacesUIUtils, "showBookmarkDialog") + .returns(Promise.resolve()); + + info("Moving existing Bookmark from position [1] to [0] on Toolbar"); + let urlMoveNotification = PlacesTestUtils.waitForNotification( + "bookmark-moved", + events => + events.some( + e => + e.parentGuid === PlacesUtils.bookmarks.toolbarGuid && + e.oldParentGuid === PlacesUtils.bookmarks.toolbarGuid && + e.oldIndex == 1 && + e.index == 0 + ) + ); + + EventUtils.synthesizeDrop( + placesItems, + placesItems.childNodes[0], + [ + [ + { + type: "text/x-moz-place", + data: PlacesUtils.wrapNode( + placesItems.childNodes[1]._placesNode, + "text/x-moz-place" + ), + }, + ], + ], + "move", + window + ); + + await urlMoveNotification; + Assert.ok(spy.notCalled, "ShowBookmarksDialog not called on move for url"); + + info("Moving existing Bookmarklet from position [2] to [1] on Toolbar"); + let bookmarkletMoveNotificatio = PlacesTestUtils.waitForNotification( + "bookmark-moved", + events => + events.some( + e => + e.parentGuid === PlacesUtils.bookmarks.toolbarGuid && + e.oldParentGuid === PlacesUtils.bookmarks.toolbarGuid && + e.oldIndex == 2 && + e.index == 1 + ) + ); + + EventUtils.synthesizeDrop( + toolbar, + placesItems.childNodes[1], + [ + [ + { + type: "text/x-moz-place", + data: PlacesUtils.wrapNode( + placesItems.childNodes[2]._placesNode, + "text/x-moz-place" + ), + }, + ], + ], + "move", + window + ); + + await bookmarkletMoveNotificatio; + Assert.ok(spy.notCalled, "ShowBookmarksDialog not called on move for url"); + sandbox.restore(); +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js new file mode 100644 index 0000000000..1f958989cf --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check flavors priority when dropping a url/title tuple on the toolbar. + +function getDataForType(url, title, type) { + switch (type) { + case "text/x-moz-url": + return `${url}\n${title}`; + case "text/plain": + return url; + case "text/html": + return `<a href="${url}">${title}</a>`; + } + throw new Error("Unknown mime type"); +} + +async function testDragDrop(effect, mimeTypes) { + const url = "https://www.mozilla.org/drag_drop_test/"; + const title = "Drag & Drop Test"; + let promiseItemAddedNotification = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(e => e.url == url) + ); + + // Ensure there's no bookmark initially + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + Assert.ok(!bookmark, "There should not be a bookmark to the given URL"); + + // We use the toolbar as the drag source, as we just need almost any node + // to simulate the drag. + let toolbar = document.getElementById("PersonalToolbar"); + EventUtils.synthesizeDrop( + toolbar, + document.getElementById("PlacesToolbarItems"), + [mimeTypes.map(type => ({ type, data: getDataForType(url, title, type) }))], + effect, + window + ); + + await promiseItemAddedNotification; + + // Verify that the drop produces exactly one bookmark. + bookmark = await PlacesUtils.bookmarks.fetch({ url }); + Assert.ok(bookmark, "There should be exactly one bookmark"); + Assert.equal(bookmark.url, url, "Check bookmark URL is correct"); + Assert.equal(bookmark.title, title, "Check bookmark title was preserved"); + await PlacesUtils.bookmarks.remove(bookmark); +} + +add_task(async function test() { + registerCleanupFunction(() => PlacesUtils.bookmarks.eraseEverything()); + + // Make sure the bookmarks bar is visible and restore its state on cleanup. + let toolbar = document.getElementById("PersonalToolbar"); + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + + // Test a bookmark drop for all of the mime types and effects. + let mimeTypes = ["text/plain", "text/html", "text/x-moz-url"]; + let effects = ["move", "copy", "link"]; + for (let effect of effects) { + await testDragDrop(effect, mimeTypes); + } +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js new file mode 100644 index 0000000000..729e456ca0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test() { + // Make sure the bookmarks bar is visible and restore its state on cleanup. + let toolbar = document.getElementById("PersonalToolbar"); + ok(toolbar, "PersonalToolbar should not be null"); + + await PlacesUtils.bookmarks.eraseEverything(); + + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + // Setup the node we will use to be dropped. The actual node used does not + // matter because we will set its data, effect, and mimeType manually. + let placesItems = document.getElementById("PlacesToolbarItems"); + Assert.ok(placesItems, "PlacesToolbarItems should not be null"); + let simulateDragDrop = async function (aEffect, aMimeType) { + let urls = [ + "https://example.com/1/", + `javascript: (() => {alert('Hello, World!');})();`, + "https://example.com/2/", + ]; + + let data = urls.map(spec => spec + "\n" + spec).join("\n"); + + EventUtils.synthesizeDrop( + toolbar, + placesItems, + [[{ type: aMimeType, data }]], + aEffect, + window + ); + await PlacesTestUtils.promiseAsyncUpdates(); + for (let url of urls) { + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + Assert.ok(!bookmark, "There should be no bookmark"); + } + }; + + // Simulate a bookmark drop for all of the mime types and effects. + let mimeType = ["text/x-moz-url"]; + let effects = ["copy", "link"]; + for (let effect of effects) { + await simulateDragDrop(effect, mimeType); + } +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_text.js b/browser/components/places/tests/browser/browser_toolbar_drop_text.js new file mode 100644 index 0000000000..ba7a7127b0 --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_drop_text.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test() { + // Make sure the bookmarks bar is visible and restore its state on cleanup. + let toolbar = document.getElementById("PersonalToolbar"); + ok(toolbar, "PersonalToolbar should not be null"); + + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + + // Setup the node we will use to be dropped. The actual node used does not + // matter because we will set its data, effect, and mimeType manually. + let placesItems = document.getElementById("PlacesToolbarItems"); + ok(placesItems, "PlacesToolbarItems should not be null"); + Assert.equal( + placesItems.localName, + "scrollbox", + "PlacesToolbarItems should not be null" + ); + + /** + * Simulates a drop of a URI onto the bookmarks bar. + * + * @param {string} aEffect + * The effect to use for the drop operation: move, copy, or link. + * @param {string} aMimeType + * The mime type to use for the drop operation. + */ + let simulateDragDrop = async function (aEffect, aMimeType) { + const url = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7"; + let promiseItemAddedNotification = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url: eventUrl }) => eventUrl == url) + ); + + // We use the toolbar as the drag source, as we just need almost any node + // to simulate the drag. The actual data for the drop is passed via the + // drag data. Note: The toolbar is used rather than another bookmark node, + // as we need something that is immovable from a places perspective, as this + // forces the move into a copy. + EventUtils.synthesizeDrop( + toolbar, + placesItems, + [[{ type: aMimeType, data: url }]], + aEffect, + window + ); + + await promiseItemAddedNotification; + + // Verify that the drop produces exactly one bookmark. + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + Assert.ok(bookmark, "There should be exactly one bookmark"); + + await PlacesUtils.bookmarks.remove(bookmark.guid); + + // Verify that we removed the bookmark successfully. + Assert.equal( + await PlacesUtils.bookmarks.fetch({ url }), + null, + "URI should be removed" + ); + }; + + /** + * Simulates a drop of multiple URIs onto the bookmarks bar. + * + * @param {string} aEffect + * The effect to use for the drop operation: move, copy, or link. + * @param {string} aMimeType + * The mime type to use for the drop operation. + */ + let simulateDragDropMultiple = async function (aEffect, aMimeType) { + const urls = [ + "http://www.mozilla.org/C54263C6-A484-46CF-8E2B-FE131586348A", + "http://www.mozilla.org/71381257-61E6-4376-AF7C-BF3C5FD8870D", + "http://www.mozilla.org/091A88BD-5743-4C16-A005-3D2EA3A3B71E", + ]; + let data; + if (aMimeType == "text/x-moz-url") { + data = urls.map(spec => spec + "\n" + spec).join("\n"); + } else { + data = urls.join("\n"); + } + + let promiseItemAddedNotification = PlacesTestUtils.waitForNotification( + "bookmark-added", + events => events.some(({ url }) => url == urls[2]) + ); + + // See notes for EventUtils.synthesizeDrop in simulateDragDrop(). + EventUtils.synthesizeDrop( + toolbar, + placesItems, + [[{ type: aMimeType, data }]], + aEffect, + window + ); + + await promiseItemAddedNotification; + + // Verify that the drop produces exactly one bookmark per each URL. + for (let url of urls) { + let bookmark = await PlacesUtils.bookmarks.fetch({ url }); + Assert.equal( + typeof bookmark, + "object", + "There should be exactly one bookmark" + ); + + await PlacesUtils.bookmarks.remove(bookmark.guid); + + // Verify that we removed the bookmark successfully. + Assert.equal( + await PlacesUtils.bookmarks.fetch({ url }), + null, + "URI should be removed" + ); + } + }; + + // Simulate a bookmark drop for all of the mime types and effects. + let mimeTypes = ["text/plain", "text/x-moz-url"]; + let effects = ["move", "copy", "link"]; + for (let effect of effects) { + for (let mimeType of mimeTypes) { + await simulateDragDrop(effect, mimeType); + await simulateDragDropMultiple(effect, mimeType); + } + } +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js b/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js new file mode 100644 index 0000000000..5bedd02a83 --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that recently added bookmarks can be opened. + */ + +const BASE_URL = + "http://example.org/browser/browser/components/places/tests/browser/"; +const bookmarkItems = [ + { + url: `${BASE_URL}bookmark_dummy_1.html`, + title: "Custom Title 1", + }, + { + url: `${BASE_URL}bookmark_dummy_2.html`, + title: "Custom Title 2", + }, +]; +let openedTabs = []; + +async function openBookmarksPanelInLibraryToolbarButton() { + let libraryBtn = document.getElementById("library-button"); + libraryBtn.click(); + let libView = document.getElementById("appMenu-libraryView"); + let viewShownPromise = BrowserTestUtils.waitForEvent(libView, "ViewShown"); + await viewShownPromise; + + let bookmarksButton; + await TestUtils.waitForCondition(() => { + bookmarksButton = document.getElementById( + "appMenu-library-bookmarks-button" + ); + return bookmarksButton; + }, "Should have the library bookmarks button"); + bookmarksButton.click(); + + let BookmarksView = document.getElementById("PanelUI-bookmarks"); + let viewRecentPromise = BrowserTestUtils.waitForEvent( + BookmarksView, + "ViewShown" + ); + await viewRecentPromise; +} + +async function openBookmarkedItemInNewTab(itemFromMenu) { + let placesContext = document.getElementById("placesContext"); + let openContextMenuPromise = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(itemFromMenu, { + button: 2, + type: "contextmenu", + }); + await openContextMenuPromise; + info("Opened context menu"); + + let tabCreatedPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + let openInNewTabOption = document.getElementById("placesContext_open:newtab"); + placesContext.activateItem(openInNewTabOption); + info("Click open in new tab"); + + let lastOpenedTab = await tabCreatedPromise; + Assert.equal( + lastOpenedTab.linkedBrowser.currentURI.spec, + itemFromMenu._placesNode.uri, + "Should have opened the correct URI" + ); + openedTabs.push(lastOpenedTab); +} + +async function closeLibraryMenu() { + let libView = document.getElementById("appMenu-libraryView"); + let viewHiddenPromise = BrowserTestUtils.waitForEvent(libView, "ViewHiding"); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + await viewHiddenPromise; +} + +async function closeTabs() { + for (var i = 0; i < openedTabs.length; i++) { + await gBrowser.removeTab(openedTabs[i]); + } + Assert.equal(gBrowser.tabs.length, 1, "Should close all opened tabs"); +} + +async function getRecentlyBookmarkedItems() { + let historyMenu = document.getElementById("panelMenu_bookmarksMenu"); + let items = historyMenu.querySelectorAll("toolbarbutton"); + Assert.ok(items, "Recently bookmarked items should exists"); + + await TestUtils.waitForCondition( + () => items[0].attributes !== "undefined", + "Custom bookmark exists" + ); + + if (items) { + Assert.equal( + items[0]._placesNode.uri, + bookmarkItems[1].url, + "Should match the expected url" + ); + Assert.equal( + items[0].getAttribute("label"), + bookmarkItems[1].title, + "Should be the expected title" + ); + Assert.equal( + items[1]._placesNode.uri, + bookmarkItems[0].url, + "Should match the expected url" + ); + Assert.equal( + items[1].getAttribute("label"), + bookmarkItems[0].title, + "Should be the expected title" + ); + } + return Array.from(items).slice(0, 2); +} + +add_setup(async function () { + let libraryButton = CustomizableUI.getPlacementOfWidget("library-button"); + if (!libraryButton) { + CustomizableUI.addWidgetToArea( + "library-button", + CustomizableUI.AREA_NAVBAR + ); + } + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: bookmarkItems, + }); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + CustomizableUI.reset(); + }); +}); + +add_task(async function test_recently_added() { + await openBookmarksPanelInLibraryToolbarButton(); + + let historyItems = await getRecentlyBookmarkedItems(); + + for (let item of historyItems) { + await openBookmarkedItemInNewTab(item); + } + + await closeLibraryMenu(); + + registerCleanupFunction(async () => { + await closeTabs(); + }); +}); diff --git a/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js b/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js new file mode 100644 index 0000000000..89171deb1f --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js @@ -0,0 +1,601 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const bookmarksInfo = [ + { + title: "firefox", + url: "http://example.com", + }, + { + title: "rules", + url: "http://example.com/2", + }, + { + title: "yo", + url: "http://example.com/2", + }, +]; + +/** + * Test showing the "Other Bookmarks" folder in the bookmarks toolbar. + */ + +// Setup. +add_setup(async function () { + // Disable window occlusion. See bug 1733955 / bug 1779559. + if (navigator.platform.indexOf("Win") == 0) { + await SpecialPowers.pushPrefEnv({ + set: [["widget.windows.window_occlusion_tracking.enabled", false]], + }); + } + + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + await setupBookmarksToolbar(); + + // Cleanup. + registerCleanupFunction(async () => { + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Test the "Other Bookmarks" folder is shown in the toolbar when +// bookmarks are stored under that folder. +add_task(async function testShowingOtherBookmarksInToolbar() { + info("Check the initial state of the Other Bookmarks folder."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + await setupBookmarksToolbar(win); + ok( + !win.document.getElementById("OtherBookmarks"), + "Shouldn't have an Other Bookmarks button." + ); + await BrowserTestUtils.closeWindow(win); + + info("Check visibility of an empty Other Bookmarks folder."); + await testIsOtherBookmarksHidden(true); + + info("Ensure folder appears in toolbar when a new bookmark is added."); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + await testIsOtherBookmarksHidden(false); + + info("Ensure folder disappears from toolbar when no bookmarks are present."); + await PlacesUtils.bookmarks.remove(bookmarks); + await testIsOtherBookmarksHidden(true); +}); + +// Test that folder visibility is correct when moving bookmarks to an empty +// "Other Bookmarks" folder and vice versa. +add_task(async function testOtherBookmarksVisibilityWhenMovingBookmarks() { + info("Add bookmarks to Bookmarks Toolbar."); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: bookmarksInfo, + }); + await testIsOtherBookmarksHidden(true); + + info("Move toolbar bookmarks to Other Bookmarks folder."); + await PlacesUtils.bookmarks.moveToFolder( + bookmarks.map(b => b.guid), + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + await testIsOtherBookmarksHidden(false); + + info("Move bookmarks from Other Bookmarks back to the toolbar."); + await PlacesUtils.bookmarks.moveToFolder( + bookmarks.map(b => b.guid), + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + await testIsOtherBookmarksHidden(true); +}); + +// Test OtherBookmarksPopup in toolbar. +add_task(async function testOtherBookmarksMenuPopup() { + info("Add bookmarks to Other Bookmarks folder."); + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + + await testIsOtherBookmarksHidden(false); + + info("Check the popup menu has correct number of children."); + await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks"); + testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 3); + await closeMenuPopup("#OtherBookmarksPopup"); + + info("Remove a bookmark."); + await PlacesUtils.bookmarks.remove(bookmarks[0]); + + await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks"); + testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 2); + await closeMenuPopup("#OtherBookmarksPopup"); +}); + +// Test that folders in the Other Bookmarks folder expand +add_task(async function testFolderPopup() { + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "example", + url: "http://example.com/3", + }, + ], + }, + ], + }); + + info("Check for popup showing event when folder menuitem is selected."); + await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks"); + await openMenuPopup( + "#OtherBookmarksPopup menu menupopup", + "#OtherBookmarksPopup menu" + ); + ok(true, "Folder menu stored in Other Bookmarks expands."); + testNumberOfMenuPopupChildren("#OtherBookmarksPopup menu menupopup", 1); + await closeMenuPopup("#OtherBookmarksPopup"); +}); + +add_task(async function testOnlyShowOtherFolderInBookmarksToolbar() { + await setupBookmarksToolbar(); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + + await testIsOtherBookmarksHidden(false); + + // Test that moving the personal-bookmarks widget out of the + // Bookmarks Toolbar will hide the "Other Bookmarks" folder. + let widgetId = "personal-bookmarks"; + CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR); + await testIsOtherBookmarksHidden(true); + + CustomizableUI.reset(); + await testIsOtherBookmarksHidden(false); +}); + +// Test that the menu popup updates when menu items are deleted from it while +// it's open. +add_task(async function testDeletingMenuItems() { + await setupBookmarksToolbar(); + + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + + await testIsOtherBookmarksHidden(false); + + await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks"); + testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 3); + + info("Open context menu for popup."); + let placesContext = document.getElementById("placesContext"); + let popupEventPromise = BrowserTestUtils.waitForPopupEvent( + placesContext, + "shown" + ); + let menuitem = document.querySelector("#OtherBookmarksPopup menuitem"); + EventUtils.synthesizeMouseAtCenter(menuitem, { type: "contextmenu" }); + await popupEventPromise; + + info("Delete bookmark menu item from popup."); + let deleteMenuBookmark = document.getElementById( + "placesContext_deleteBookmark" + ); + placesContext.activateItem(deleteMenuBookmark); + + await TestUtils.waitForCondition(() => { + let popup = document.querySelector("#OtherBookmarksPopup"); + let items = popup.querySelectorAll("menuitem"); + return items.length === 2; + }, "Failed to delete bookmark menuitem. Expected 2 menu items after deletion."); + ok(true, "Menu item was removed from the popup."); + await closeMenuPopup("#OtherBookmarksPopup"); +}); + +add_task(async function no_errors_when_bookmarks_placed_in_palette() { + CustomizableUI.removeWidgetFromArea("personal-bookmarks"); + + let consoleErrors = 0; + + let errorListener = { + observe(error) { + ok(false, `${error.message}, ${error.stack}, ${JSON.stringify(error)}`); + consoleErrors++; + }, + }; + Services.console.registerListener(errorListener); + + let bookmarks = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: bookmarksInfo, + }); + is(consoleErrors, 0, "There should be no console errors"); + + Services.console.unregisterListener(errorListener); + await PlacesUtils.bookmarks.remove(bookmarks); + CustomizableUI.reset(); +}); + +// Test "Show Other Bookmarks" menu item visibility in toolbar context menu. +add_task(async function testShowingOtherBookmarksContextMenuItem() { + await setupBookmarksToolbar(); + + info("Add bookmark to Other Bookmarks."); + let bookmark = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [{ title: "firefox", url: "http://example.com" }], + }); + + info("'Show Other Bookmarks' menu item should be checked by default."); + await testOtherBookmarksCheckedState(true); + + info("Toggle off showing the Other Bookmarks folder."); + await selectShowOtherBookmarksMenuItem(); + await testOtherBookmarksCheckedState(false); + await testIsOtherBookmarksHidden(true); + + info("Toggle on showing the Other Bookmarks folder."); + await selectShowOtherBookmarksMenuItem(); + await testOtherBookmarksCheckedState(true); + await testIsOtherBookmarksHidden(false); + + info( + "Ensure 'Show Other Bookmarks' isn't shown when Other Bookmarks is empty." + ); + await PlacesUtils.bookmarks.remove(bookmark); + await testIsOtherBookmarksMenuItemEnabled(false); + + info("Add a bookmark to the empty Other Bookmarks folder."); + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [{ title: "firefox", url: "http://example.com" }], + }); + await testIsOtherBookmarksMenuItemEnabled(true); + + info( + "Ensure that displaying Other Bookmarks is consistent across separate windows." + ); + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForCondition(() => { + let otherBookmarks = newWin.document.getElementById("OtherBookmarks"); + return otherBookmarks && !otherBookmarks.hidden; + }, "Other Bookmarks folder failed to show in other window."); + + info("Hide the Other Bookmarks folder from the original window."); + await selectShowOtherBookmarksMenuItem(); + + await TestUtils.waitForCondition(() => { + let otherBookmarks = newWin.document.getElementById("OtherBookmarks"); + return !otherBookmarks || otherBookmarks.hidden; + }, "Other Bookmarks folder failed to be hidden in other window."); + ok(true, "Other Bookmarks was successfully hidden in other window."); + + info("Show the Other Bookmarks folder from the original window."); + await selectShowOtherBookmarksMenuItem(); + + await TestUtils.waitForCondition(() => { + let otherBookmarks = newWin.document.getElementById("OtherBookmarks"); + return otherBookmarks && !otherBookmarks.hidden; + }, "Other Bookmarks folder failed to be shown in other window."); + ok(true, "Other Bookmarks was successfully shown in other window."); + + await BrowserTestUtils.closeWindow(newWin); +}); + +// Test 'Show Other Bookmarks' isn't shown when pref is false. +add_task(async function showOtherBookmarksMenuItemPrefDisabled() { + await setupBookmarksToolbar(); + await testIsOtherBookmarksMenuItemEnabled(false); +}); + +// Test that node visibility for toolbar overflow is consisten when the "Other Bookmarks" +// folder is shown/hidden. +add_task(async function testOtherBookmarksToolbarOverFlow() { + await setupBookmarksToolbar(); + + info( + "Ensure that visible nodes when showing/hiding Other Bookmarks is consistent across separate windows." + ); + // Add bookmarks to other bookmarks + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [{ title: "firefox", url: "http://example.com" }], + }); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + // Add bookmarks to the toolbar + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: Array(100) + .fill("") + .map((_, i) => ({ title: `test ${i}`, url: `http:example.com/${i}` })), + }); + + info("Hide the Other Bookmarks folder from the original window."); + await selectShowOtherBookmarksMenuItem("#OtherBookmarks"); + await BrowserTestUtils.waitForEvent( + document.getElementById("PersonalToolbar"), + "BookmarksToolbarVisibilityUpdated" + ); + ok(true, "Nodes successfully updated for both windows."); + await testUpdatedNodeVisibility(newWin); + + info("Show the Other Bookmarks folder from the original window."); + await selectShowOtherBookmarksMenuItem("#PlacesChevron"); + await BrowserTestUtils.waitForEvent( + document.getElementById("PersonalToolbar"), + "BookmarksToolbarVisibilityUpdated" + ); + ok(true, "Nodes successfully updated for both windows."); + await testUpdatedNodeVisibility(newWin); + + await BrowserTestUtils.closeWindow(newWin); +}); + +/** + * Tests whether or not the "Other Bookmarks" folder is visible. + * + * @param {boolean} expected + * The expected state of the Other Bookmarks folder. There are 3: + * - the folder node isn't initialized and is therefore not visible, + * - the folder node is initialized and is hidden + * - the folder node is initialized and is visible + */ +async function testIsOtherBookmarksHidden(expected) { + info("Test whether or not the 'Other Bookmarks' folder is visible."); + + // Ensure the toolbar is visible. + let toolbar = document.getElementById("PersonalToolbar"); + await promiseSetToolbarVisibility(toolbar, true); + + let otherBookmarks = document.getElementById("OtherBookmarks"); + + await TestUtils.waitForCondition(() => { + otherBookmarks = document.getElementById("OtherBookmarks"); + let isHidden = !otherBookmarks || otherBookmarks.hidden; + return isHidden === expected; + }, "Other Bookmarks folder failed to change hidden state."); + + ok(true, `Other Bookmarks folder "hidden" state should be ${expected}.`); +} + +/** + * Tests number of menu items in Other Bookmarks popup. + * + * @param {string} selector + * The selector for getting the menupopup element we want to test. + * @param {number} expected + * The expected number of menuitem elements inside the menupopup. + */ +function testNumberOfMenuPopupChildren(selector, expected) { + let popup = document.querySelector(selector); + let items = popup.querySelectorAll("menuitem"); + + is( + items.length, + expected, + `Number of menu items for ${selector} should be ${expected}.` + ); +} + +/** + * Test helper for checking the 'checked' state of the "Show Other Bookmarks" menu item + * after selecting it from the context menu. + * + * @param {boolean} expectedCheckedState + * Whether or not the menu item is checked. + */ +async function testOtherBookmarksCheckedState(expectedCheckedState) { + info("Check 'Show Other Bookmarks' menu item state"); + await openToolbarContextMenu(); + + let otherBookmarksMenuItem = document.querySelector( + "#show-other-bookmarks_PersonalToolbar" + ); + + is( + otherBookmarksMenuItem.getAttribute("checked"), + `${expectedCheckedState}`, + `Other Bookmarks item's checked state should be ${expectedCheckedState}` + ); + + await closeToolbarContextMenu(); +} + +/** + * Test helper for checking whether or not the 'Show Other Bookmarks' menu item + * is enabled in the toolbar's context menu. + * + * @param {boolean} expected + * Whether or not the menu item is enabled in the toolbar conext menu. + */ +async function testIsOtherBookmarksMenuItemEnabled(expected) { + await openToolbarContextMenu(); + + let otherBookmarksMenuItem = document.querySelector( + "#show-other-bookmarks_PersonalToolbar" + ); + + is( + !otherBookmarksMenuItem.disabled, + expected, + "'Show Other Bookmarks' menu item appearance state is correct." + ); + + await closeToolbarContextMenu(); +} + +/** + * Helper for opening a menu popup. + * + * @param {string} popupSelector + * The selector for the menupopup element we want to open. + * @param {string} targetSelector + * The selector for the element with the popup showing event. + */ +async function openMenuPopup(popupSelector, targetSelector) { + let popup = document.querySelector(popupSelector); + let target = document.querySelector(targetSelector); + + EventUtils.synthesizeMouseAtCenter(target, {}); + + await BrowserTestUtils.waitForPopupEvent(popup, "shown"); +} + +/** + * Helper for closing a menu popup. + * + * @param {string} popupSelector + * The selector for the menupopup element we want to close. + */ +async function closeMenuPopup(popupSelector) { + let popup = document.querySelector(popupSelector); + + info("Closing menu popup."); + popup.hidePopup(); + await BrowserTestUtils.waitForPopupEvent(popup, "hidden"); +} + +/** + * Helper for opening the toolbar context menu. + * + * @param {string} toolbarSelector + * Optional. The selector for the toolbar context menu. + * Defaults to #PlacesToolbarItems. + */ +async function openToolbarContextMenu(toolbarSelector = "#PlacesToolbarItems") { + let contextMenu = document.getElementById("placesContext"); + let toolbar = document.querySelector(toolbarSelector); + let openToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + + // Use the end of the toolbar because the beginning (and even middle, on + // some resolutions) might be occluded by the empty toolbar message, which + // has a different context menu. + let bounds = toolbar.getBoundingClientRect(); + EventUtils.synthesizeMouse(toolbar, bounds.width - 5, 5, { + type: "contextmenu", + }); + + await openToolbarContextMenuPromise; +} + +/** + * Helper for closing the toolbar context menu. + */ +async function closeToolbarContextMenu() { + let contextMenu = document.getElementById("placesContext"); + let closeToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "hidden" + ); + contextMenu.hidePopup(); + await closeToolbarContextMenuPromise; +} + +/** + * Helper for setting up the bookmarks toolbar state. This ensures the beginning + * of a task will always have the bookmark toolbar in a state that makes the + * Other Bookmarks folder testable. + * + * @param {object} [win] + * The window object to use. + */ +async function setupBookmarksToolbar(win = window) { + let toolbar = win.document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + await PlacesUtils.bookmarks.eraseEverything(); +} + +/** + * Helper for selecting the "Show Other Bookmarks" menu item from the bookmarks + * toolbar context menu. + * + * @param {string} selector + * Optional. The selector for the node that triggers showing the + * "Show Other Bookmarks" context menu item in the toolbar. + * Defaults to #PlacesToolbarItem when `openToolbarContextMenu` is + * called. + */ +async function selectShowOtherBookmarksMenuItem(selector) { + info("Select 'Show Other Bookmarks' menu item"); + await openToolbarContextMenu(selector); + + let otherBookmarksMenuItem = document.querySelector( + "#show-other-bookmarks_PersonalToolbar" + ); + let contextMenu = document.getElementById("placesContext"); + + contextMenu.activateItem(otherBookmarksMenuItem); + + await BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden"); + await closeToolbarContextMenu(); +} + +/** + * Test helper for node visibility in the bookmarks toolbar between two windows. + * + * @param {Window} otherWin + * The other window whose toolbar items we want to compare with. + */ +function testUpdatedNodeVisibility(otherWin) { + // Get visible toolbar nodes for both the current and other windows. + let toolbarItems = document.getElementById("PlacesToolbarItems"); + let currentVisibleNodes = []; + + for (let node of toolbarItems.children) { + if (node.style.visibility === "visible") { + currentVisibleNodes.push(node); + } + } + + let otherToolbarItems = + otherWin.document.getElementById("PlacesToolbarItems"); + let otherVisibleNodes = []; + + for (let node of otherToolbarItems.children) { + if (node.style.visibility === "visible") { + otherVisibleNodes.push(node); + } + } + + let lastIdx = otherVisibleNodes.length - 1; + + is( + currentVisibleNodes[lastIdx]?.bookmarkGuid, + otherVisibleNodes[lastIdx]?.bookmarkGuid, + "Last visible toolbar bookmark is the same in both windows." + ); +} diff --git a/browser/components/places/tests/browser/browser_toolbar_overflow.js b/browser/components/places/tests/browser/browser_toolbar_overflow.js new file mode 100644 index 0000000000..3f16c2a126 --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_overflow.js @@ -0,0 +1,436 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the bookmarks toolbar overflow. + */ + +var gToolbar = document.getElementById("PersonalToolbar"); +var gChevron = document.getElementById("PlacesChevron"); + +const BOOKMARKS_COUNT = 250; + +add_setup(async function () { + let wasCollapsed = gToolbar.collapsed; + await PlacesUtils.bookmarks.eraseEverything(); + + // Add bookmarks. + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.toolbarGuid, + children: Array(BOOKMARKS_COUNT) + .fill("") + .map((_, i) => ({ url: `http://test.places.y${i}/` })), + }); + + // Toggle the bookmarks toolbar so that we start from a stable situation and + // are not affected by all bookmarks removal. + await toggleToolbar(false); + await toggleToolbar(true); + + registerCleanupFunction(async () => { + if (wasCollapsed) { + await toggleToolbar(false); + } + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_overflow() { + // Check that the overflow chevron is visible. + Assert.ok(!gChevron.collapsed, "The overflow chevron should be visible"); + let children = getPlacesChildren(); + Assert.ok( + children.length < BOOKMARKS_COUNT, + "Not all the nodes should be built by default" + ); + let visibleNodes = []; + for (let node of children) { + if (getComputedStyle(node).visibility == "visible") { + visibleNodes.push(node); + } + } + Assert.ok( + visibleNodes.length < children.length, + `The number of visible nodes (${visibleNodes.length}) should be smaller than the number of built nodes (${children.length})` + ); + + await test_index( + "Node at the last visible index", + visibleNodes.length - 1, + "visible" + ); + await test_index( + "Node at the first invisible index", + visibleNodes.length, + "hidden" + ); + await test_index("First non-built node", children.length, undefined); + await test_index("Later non-built node", children.length + 1, undefined); + + await test_move_index( + "Move node from last visible to first hidden", + visibleNodes.length - 1, + visibleNodes.length, + "visible", + "hidden" + ); + await test_move_index( + "Move node from fist visible to last built", + 0, + children.length - 1, + "visible", + "hidden" + ); + await test_move_index( + "Move node from fist visible to first non built", + 0, + children.length, + "visible", + undefined + ); +}); + +add_task(async function test_separator_first() { + await toggleToolbar(false); + // Check that if a separator is the first node, we still calculate overflow + // properly. + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: 0, + }); + await toggleToolbar(true, 2); + + let children = getPlacesChildren(); + Assert.greater(children.length, 2, "Multiple elements are visible"); + Assert.equal( + children[1]._placesNode.uri, + "http://test.places.y0/", + "Found the first bookmark" + ); + Assert.equal( + getComputedStyle(children[1]).visibility, + "visible", + "The first bookmark is visible" + ); + + await PlacesUtils.bookmarks.remove(bm); +}); + +add_task(async function test_newWindow_noOverflow() { + info( + "Check toolbar in a new widow when it was already visible and not overflowed" + ); + Assert.ok(!gToolbar.collapsed, "Toolbar is not collapsed in original window"); + await PlacesUtils.bookmarks.eraseEverything(); + // Add a single bookmark. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://toolbar.overflow/", + title: "Example", + }); + // Add a favicon for the bookmark. + let favicon = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" + + "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; + await PlacesTestUtils.addFavicons( + new Map([["http://toolbar.overflow/", favicon]]) + ); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + try { + let toolbar = win.document.getElementById("PersonalToolbar"); + Assert.ok(!toolbar.collapsed, "The toolbar is not collapsed"); + let content = win.document.getElementById("PlacesToolbarItems"); + await TestUtils.waitForCondition(() => { + return ( + content.children.length == 1 && + content.children[0].hasAttribute("image") + ); + }); + let chevron = win.document.getElementById("PlacesChevron"); + Assert.ok(chevron.collapsed, "The chevron should be collapsed"); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); + +async function test_index(desc, index, expected) { + info(desc); + let children = getPlacesChildren(); + let originalLen = children.length; + let nodeExisted = children.length > index; + let previousNodeIsVisible = + nodeExisted && + getComputedStyle(children[index - 1]).visibility == "visible"; + let promise = promiseUpdateVisibility( + expected == "visible" || previousNodeIsVisible + ); + + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://test.places.added/", + index, + }); + Assert.equal(bm.index, index, "Sanity check the bookmark index"); + await promise; + children = getPlacesChildren(); + + if (!expected) { + Assert.ok( + children.length <= index, + "The new node should not have been added" + ); + } else { + Assert.equal( + children[index]._placesNode.bookmarkGuid, + bm.guid, + "Found the added bookmark at the expected position" + ); + Assert.equal( + getComputedStyle(children[index]).visibility, + expected, + `The bookmark node should be ${expected}` + ); + } + Assert.equal( + children.length, + originalLen, + "Number of built nodes should stay the same" + ); + + info("Remove the node"); + promise = promiseUpdateVisibility(expected == "visible"); + + await PlacesUtils.bookmarks.remove(bm); + await promise; + children = getPlacesChildren(); + + if (expected && nodeExisted) { + Assert.equal( + children[index]._placesNode.uri, + `http://test.places.y${index}/`, + "Found the previous bookmark at the expected position" + ); + Assert.equal( + getComputedStyle(children[index]).visibility, + expected, + `The bookmark node should be ${expected}` + ); + } + Assert.equal( + children.length, + originalLen, + "Number of built nodes should stay the same" + ); +} + +async function test_move_index(desc, fromIndex, toIndex, original, expected) { + info(desc); + let children = getPlacesChildren(); + let originalLen = children.length; + let movedGuid = children[fromIndex]._placesNode.bookmarkGuid; + let existingGuid = children[toIndex] + ? children[toIndex]._placesNode.bookmarkGuid + : null; + let existingIndex = fromIndex < toIndex ? toIndex - 1 : toIndex + 1; + + Assert.equal( + getComputedStyle(children[fromIndex]).visibility, + original, + `The bookmark node should be ${original}` + ); + let promise = promiseUpdateVisibility( + original == "visible" || expected == "visible" + ); + + await PlacesUtils.bookmarks.update({ + guid: movedGuid, + index: toIndex, + }); + await promise; + children = getPlacesChildren(); + + if (!expected) { + Assert.ok( + children.length <= toIndex, + "Node in the new position is not expected" + ); + Assert.ok( + children[originalLen - 1], + "We should keep number of built nodes consistent" + ); + } else { + Assert.equal( + children[toIndex]._placesNode.bookmarkGuid, + movedGuid, + "Found the moved bookmark at the expected position" + ); + Assert.equal( + getComputedStyle(children[toIndex]).visibility, + expected, + `The destination bookmark node should be ${expected}` + ); + } + Assert.equal( + getComputedStyle(children[fromIndex]).visibility, + original, + `The origin bookmark node should be ${original}` + ); + if (existingGuid) { + Assert.equal( + children[existingIndex]._placesNode.bookmarkGuid, + existingGuid, + "Found the pushed away bookmark at the expected position" + ); + } + Assert.equal( + children.length, + originalLen, + "Number of built nodes should stay the same" + ); + + info("Moving back the node"); + promise = promiseUpdateVisibility( + original == "visible" || expected == "visible" + ); + + await PlacesUtils.bookmarks.update({ + guid: movedGuid, + index: fromIndex, + }); + await promise; + children = getPlacesChildren(); + + Assert.equal( + children[fromIndex]._placesNode.bookmarkGuid, + movedGuid, + "Found the moved bookmark at the expected position" + ); + if (expected) { + Assert.equal( + getComputedStyle(children[toIndex]).visibility, + expected, + `The bookmark node should be ${expected}` + ); + } + Assert.equal( + getComputedStyle(children[fromIndex]).visibility, + original, + `The bookmark node should be ${original}` + ); + if (existingGuid) { + Assert.equal( + children[toIndex]._placesNode.bookmarkGuid, + existingGuid, + "Found the pushed away bookmark at the expected position" + ); + } + Assert.equal( + children.length, + originalLen, + "Number of built nodes should stay the same" + ); +} + +add_task(async function test_separator_first() { + // Check that if there are only separators, we still show nodes properly. + await toggleToolbar(false); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: 0, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: 0, + }); + await toggleToolbar(true, 2); + + let children = getPlacesChildren(); + Assert.equal(children.length, 2, "The expected elements are visible"); + Assert.equal( + getComputedStyle(children[0]).visibility, + "visible", + "The first bookmark is visible" + ); + Assert.equal( + getComputedStyle(children[1]).visibility, + "visible", + "The second bookmark is visible" + ); + + await toggleToolbar(false); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +/** + * If the passed-in condition is fulfilled, awaits for the toolbar nodes + * visibility to have been updated. + * + * @param {boolean} [condition] Awaits for visibility only if this condition is true. + * @returns {Promise} resolved when the condition is not fulfilled or the + * visilibily update happened. + */ +function promiseUpdateVisibility(condition = true) { + if (condition) { + return BrowserTestUtils.waitForEvent( + gToolbar, + "BookmarksToolbarVisibilityUpdated" + ); + } + return Promise.resolve(); +} + +/** + * Returns an array of toolbar children that are Places nodes, ignoring things + * like the chevron or other additional buttons. + * + * @returns {Array} An array of Places element nodes. + */ +function getPlacesChildren() { + return Array.prototype.filter.call( + document.getElementById("PlacesToolbarItems").children, + c => c._placesNode?.itemId + ); +} + +/** + * Toggles the toolbar on or off. + * + * @param {boolean} show Whether to show or hide the toolbar. + * @param {number} [expectedMinChildCount] Optional number of Places nodes that + * should be visible on the toolbar. + */ +async function toggleToolbar(show, expectedMinChildCount = 0) { + let promiseReady = Promise.resolve(); + if (show) { + promiseReady = promiseUpdateVisibility(); + } + + await promiseSetToolbarVisibility(gToolbar, show); + await promiseReady; + + if (show) { + if (getPlacesChildren().length < expectedMinChildCount) { + await new Promise(resolve => { + info("Waiting for bookmark elements to appear"); + let mut = new MutationObserver(mutations => { + let children = getPlacesChildren(); + info(`${children.length} bookmark elements appeared`); + if (children.length >= expectedMinChildCount) { + resolve(); + mut.disconnect(); + } + }); + mut.observe(document.getElementById("PlacesToolbarItems"), { + childList: true, + }); + }); + } + } +} diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js new file mode 100644 index 0000000000..a87f26caab --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js @@ -0,0 +1,72 @@ +CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR, + 4 +); +var bookmarksMenuButton = document.getElementById("bookmarks-menu-button"); +var BMB_menuPopup = document.getElementById("BMB_bookmarksPopup"); +var BMB_showAllBookmarks = document.getElementById("BMB_bookmarksShowAll"); +var contextMenu = document.getElementById("placesContext"); +var newBookmarkItem = document.getElementById("placesContext_new:bookmark"); + +waitForExplicitFinish(); +add_task(async function testPopup() { + info("Checking popup context menu before moving the bookmarks button"); + await checkPopupContextMenu(); + let pos = CustomizableUI.getPlacementOfWidget( + "bookmarks-menu-button" + ).position; + let target = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; + CustomizableUI.addWidgetToArea("bookmarks-menu-button", target); + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR, + pos + ); + info("Checking popup context menu after moving the bookmarks button"); + await checkPopupContextMenu(); + CustomizableUI.reset(); +}); + +async function checkPopupContextMenu() { + let clickTarget = bookmarksMenuButton; + BMB_menuPopup.setAttribute("style", "transition: none;"); + let popupShownPromise = onPopupEvent(BMB_menuPopup, "shown"); + EventUtils.synthesizeMouseAtCenter(clickTarget, {}); + info("Waiting for bookmarks menu to be shown."); + await popupShownPromise; + let contextMenuShownPromise = onPopupEvent(contextMenu, "shown"); + EventUtils.synthesizeMouseAtCenter(BMB_showAllBookmarks, { + type: "contextmenu", + button: 2, + }); + info("Waiting for context menu on bookmarks menu to be shown."); + await contextMenuShownPromise; + ok( + !newBookmarkItem.hasAttribute("disabled"), + "New bookmark item shouldn't be disabled" + ); + let contextMenuHiddenPromise = onPopupEvent(contextMenu, "hidden"); + contextMenu.hidePopup(); + BMB_menuPopup.removeAttribute("style"); + info("Waiting for context menu on bookmarks menu to be hidden."); + await contextMenuHiddenPromise; + let popupHiddenPromise = onPopupEvent(BMB_menuPopup, "hidden"); + // Can't use synthesizeMouseAtCenter because the dropdown panel is in the way + EventUtils.synthesizeKey("KEY_Escape"); + info("Waiting for bookmarks menu to be hidden."); + await popupHiddenPromise; +} + +function onPopupEvent(popup, evt) { + let fullEvent = "popup" + evt; + return new Promise(resolve => { + let onPopupHandler = e => { + if (e.target == popup) { + popup.removeEventListener(fullEvent, onPopupHandler); + resolve(); + } + }; + popup.addEventListener(fullEvent, onPopupHandler); + }); +} diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js new file mode 100644 index 0000000000..02720cfa2e --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js @@ -0,0 +1,92 @@ +/** + * This test checks that the Show in Folder context menu item in the + * bookmarks menu under the app menu actually shows the bookmark in + * its folder location in the sidebar. + */ +"use strict"; + +const TEST_PARENT_FOLDER = "The Parent Folder"; +const TEST_URL = "https://example.com/"; +const TEST_TITLE = "Test Bookmark"; + +let appMenuButton = document.getElementById("PanelUI-menu-button"); +let bookmarksAppMenu = document.getElementById("PanelUI-bookmarks"); +let sidebarWasAlreadyOpen = SidebarUI.isOpen; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); +let gCUITestUtils = new CustomizableUITestUtils(window); + +add_task(async function toolbarBookmarkShowInFolder() { + let parentFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: TEST_PARENT_FOLDER, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: parentFolder.guid, + url: TEST_URL, + title: TEST_TITLE, + }); + + // Open app menu and select bookmarks view + await gCUITestUtils.openMainMenu(); + let appMenuBookmarks = document.getElementById("appMenu-bookmarks-button"); + appMenuBookmarks.click(); + let bookmarksView = document.getElementById("PanelUI-bookmarks"); + let bmViewPromise = BrowserTestUtils.waitForEvent(bookmarksView, "ViewShown"); + await bmViewPromise; + + // Find the test bookmark and open the context menu on it + let list = document.getElementById("panelMenu_bookmarksMenu"); + let listItem = [...list.children].find(node => node.label == TEST_TITLE); + let placesContext = document.getElementById("placesContext"); + let contextPromise = BrowserTestUtils.waitForEvent( + placesContext, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(listItem, { + button: 2, + type: "contextmenu", + }); + await contextPromise; + + // Select Show in Folder and wait for the sidebar to show up + let sidebarShownPromise = BrowserTestUtils.waitForEvent( + window, + "SidebarShown" + ); + placesContext.activateItem( + document.getElementById("placesContext_showInFolder") + ); + await sidebarShownPromise; + + // Get the sidebar tree element and find the selected node + let sidebar = document.getElementById("sidebar"); + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + let treeNode = tree.selectedNode; + + Assert.equal( + treeNode.parent.bookmarkGuid, + parentFolder.guid, + "Containing folder node is correct" + ); + Assert.equal( + treeNode.title, + listItem.label, + "The bookmark title matches selected node" + ); + Assert.equal( + treeNode.uri, + TEST_URL, + "The bookmark URL matches selected node" + ); + + // Cleanup + await PlacesUtils.bookmarks.eraseEverything(); + if (!sidebarWasAlreadyOpen) { + SidebarUI.hide(); + } + await gCUITestUtils.hideMainMenu(); +}); diff --git a/browser/components/places/tests/browser/browser_views_iconsupdate.js b/browser/components/places/tests/browser/browser_views_iconsupdate.js new file mode 100644 index 0000000000..1799a9665b --- /dev/null +++ b/browser/components/places/tests/browser/browser_views_iconsupdate.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * Tests Places views (toolbar, tree) for icons update. + * The menu is not tested since it uses the same code as the toolbar. + */ + +add_task(async function () { + const PAGE_URI = NetUtil.newURI("http://places.test/"); + const ICON_URI = NetUtil.newURI( + "http://mochi.test:8888/browser/browser/components/places/tests/browser/favicon-normal16.png" + ); + + info("Uncollapse the personal toolbar if needed"); + let toolbar = document.getElementById("PersonalToolbar"); + let wasCollapsed = toolbar.collapsed; + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(async function () { + await promiseSetToolbarVisibility(toolbar, false); + }); + } + + info("Open the bookmarks sidebar"); + let sidebar = document.getElementById("sidebar"); + let promiseSidebarLoaded = new Promise(resolve => { + sidebar.addEventListener("load", resolve, { capture: true, once: true }); + }); + SidebarUI.show("viewBookmarksSidebar"); + registerCleanupFunction(() => { + SidebarUI.hide(); + }); + await promiseSidebarLoaded; + + // Add a bookmark to the bookmarks toolbar. + let bm = await PlacesUtils.bookmarks.insert({ + url: PAGE_URI, + title: "test icon", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + // The icon is read asynchronously from the network, we don't have an easy way + // to wait for that. + await new Promise(resolve => { + setTimeout(resolve, 3000); + }); + + let toolbarElt = getNodeForToolbarItem(bm.guid); + let toolbarShot1 = TestUtils.screenshotArea(toolbarElt, window); + let sidebarRect = await getRectForSidebarItem(bm.guid); + let sidebarShot1 = TestUtils.screenshotArea(sidebarRect, window); + + info("Toolbar: " + toolbarShot1); + info("Sidebar: " + sidebarShot1); + + let iconURI = await new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + PAGE_URI, + ICON_URI, + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }); + Assert.ok(iconURI.equals(ICON_URI), "Succesfully set the icon"); + + // The icon is read asynchronously from the network, we don't have an easy way + // to wait for that, thus we must poll. + await TestUtils.waitForCondition(() => { + // Assert.notEqual truncates the strings, so it is unusable here for failure + // debugging purposes. + let toolbarShot2 = TestUtils.screenshotArea(toolbarElt, window); + if (toolbarShot1 != toolbarShot2) { + info("After toolbar: " + toolbarShot2); + } + return toolbarShot1 != toolbarShot2; + }, "Waiting for the toolbar icon to update"); + + await TestUtils.waitForCondition(() => { + let sidebarShot2 = TestUtils.screenshotArea(sidebarRect, window); + if (sidebarShot1 != sidebarShot2) { + info("After sidebar: " + sidebarShot2); + } + return sidebarShot1 != sidebarShot2; + }, "Waiting for the sidebar icon to update"); +}); + +/** + * Get Element for a bookmark in the bookmarks toolbar. + * + * @param {string} guid + * GUID of the item to search. + * @returns {object} DOM Node of the element. + */ +function getNodeForToolbarItem(guid) { + return Array.from( + document.getElementById("PlacesToolbarItems").children + ).find(child => child._placesNode && child._placesNode.bookmarkGuid == guid); +} + +/** + * Get a rect for a bookmark in the bookmarks sidebar + * + * @param {string} guid + * GUID of the item to search. + * @returns {object} DOM Node of the element. + */ +async function getRectForSidebarItem(guid) { + let sidebar = document.getElementById("sidebar"); + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + tree.selectItems([guid]); + let treerect = tree.getBoundingClientRect(); + let cellrect = tree.getCoordsForCellItem( + tree.currentIndex, + tree.columns[0], + "cell" + ); + + // Adjust the position for the tree and sidebar. + return { + left: treerect.left + cellrect.left + sidebar.getBoundingClientRect().left, + top: treerect.top + cellrect.top + sidebar.getBoundingClientRect().top, + width: cellrect.width, + height: cellrect.height, + }; +} diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js new file mode 100644 index 0000000000..cce35941f3 --- /dev/null +++ b/browser/components/places/tests/browser/browser_views_liveupdate.js @@ -0,0 +1,493 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests Places views (menu, toolbar, tree) for liveupdate. + */ + +var toolbar = document.getElementById("PersonalToolbar"); +var wasCollapsed = toolbar.collapsed; + +/** + * Simulates popup opening causing it to populate. + * We cannot just use menu.open, since it would not work on Mac due to native menubar. + * + * @param {object} aPopup + * The popup element + */ +function fakeOpenPopup(aPopup) { + var popupEvent = document.createEvent("MouseEvent"); + popupEvent.initMouseEvent( + "popupshowing", + true, + true, + window, + 0, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + aPopup.dispatchEvent(popupEvent); +} + +async function testInFolder(folderGuid, prefix) { + let addedBookmarks = []; + let item = await PlacesUtils.bookmarks.insert({ + parentGuid: folderGuid, + title: `${prefix}1`, + url: `http://${prefix}1.mozilla.org/`, + }); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + + item.title = `${prefix}1_edited`; + await PlacesUtils.bookmarks.update(item); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + + addedBookmarks.push(item); + + item = await PlacesUtils.bookmarks.insert({ + parentGuid: folderGuid, + title: `${prefix}2`, + url: "place:", + }); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + + item.title = `${prefix}2_edited`; + await PlacesUtils.bookmarks.update(item); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + addedBookmarks.push(item); + + item = await PlacesUtils.bookmarks.insert({ + parentGuid: folderGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + addedBookmarks.push(item); + + item = await PlacesUtils.bookmarks.insert({ + parentGuid: folderGuid, + title: `${prefix}f`, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + + item.title = `${prefix}f_edited`; + await PlacesUtils.bookmarks.update(item); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + addedBookmarks.push(item); + + item = await PlacesUtils.bookmarks.insert({ + parentGuid: item.guid, + title: `${prefix}f1`, + url: `http://${prefix}f1.mozilla.org/`, + }); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + addedBookmarks.push(item); + + item.index = 0; + item.parentGuid = folderGuid; + await PlacesUtils.bookmarks.update(item); + await bookmarksObserver.assertViewsUpdatedCorrectly(); + + return addedBookmarks; +} + +add_task(async function test() { + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, true); + } + + // Open bookmarks menu. + var popup = document.getElementById("bookmarksMenuPopup"); + ok(popup, "Menu popup element exists"); + fakeOpenPopup(popup); + + // Open bookmarks sidebar. + await withSidebarTree("bookmarks", async () => { + // Add observers. + bookmarksObserver.handlePlacesEvents = + bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver); + PlacesUtils.observers.addListener( + ["bookmark-added", "bookmark-removed"], + bookmarksObserver.handlePlacesEvents + ); + var addedBookmarks = []; + + // MENU + info("*** Acting on menu bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm") + ); + + // TOOLBAR + info("*** Acting on toolbar bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.toolbarGuid, "tb") + ); + + // UNSORTED + info("*** Acting on unsorted bookmarks"); + addedBookmarks = addedBookmarks.concat( + await testInFolder(PlacesUtils.bookmarks.unfiledGuid, "ub") + ); + + // Remove all added bookmarks. + for (let bm of addedBookmarks) { + // If we remove an item after its containing folder has been removed, + // this will throw, but we can ignore that. + try { + await PlacesUtils.bookmarks.remove(bm); + } catch (ex) {} + await bookmarksObserver.assertViewsUpdatedCorrectly(); + } + + // Remove observers. + PlacesUtils.observers.removeListener( + ["bookmark-added", "bookmark-removed"], + bookmarksObserver.handlePlacesEvents + ); + }); + + // Collapse the personal toolbar if needed. + if (wasCollapsed) { + await promiseSetToolbarVisibility(toolbar, false); + } +}); + +/** + * The observer is where magic happens, for every change we do it will look for + * nodes positions in the affected views. + */ +var bookmarksObserver = { + _notifications: [], + + handlePlacesEvents(events) { + for (let { type, parentGuid, guid, index } of events) { + switch (type) { + case "bookmark-added": + this._notifications.push([ + "assertItemAdded", + parentGuid, + guid, + index, + ]); + break; + case "bookmark-removed": + this._notifications.push(["assertItemRemoved", parentGuid, guid]); + break; + } + } + }, + + async assertViewsUpdatedCorrectly() { + for (let notification of this._notifications) { + let assertFunction = notification.shift(); + + let views = await getViewsForFolder(notification.shift()); + Assert.greater( + views.length, + 0, + "Should have found one or more views for the parent folder." + ); + + await this[assertFunction](views, ...notification); + } + + this._notifications = []; + }, + + async assertItemAdded(views, guid, expectedIndex) { + for (let i = 0; i < views.length; i++) { + let [node, index] = searchItemInView(guid, views[i]); + Assert.notEqual(node, null, "Should have found the view in " + views[i]); + Assert.equal( + index, + expectedIndex, + "Should have found the node at the expected index" + ); + } + }, + + async assertItemRemoved(views, guid) { + for (let i = 0; i < views.length; i++) { + let [node] = searchItemInView(guid, views[i]); + Assert.equal(node, null, "Should not have found the node"); + } + }, + + async assertItemMoved(views, guid, newIndex) { + // Check that item has been moved in the correct position. + for (let i = 0; i < views.length; i++) { + let [node, index] = searchItemInView(guid, views[i]); + Assert.notEqual(node, null, "Should have found the view in " + views[i]); + Assert.equal( + index, + newIndex, + "Should have found the node at the expected index" + ); + } + }, + + async assertItemChanged(views, guid, newValue) { + let validator = function (aElementOrTreeIndex) { + if (typeof aElementOrTreeIndex == "number") { + let sidebar = document.getElementById("sidebar"); + let tree = sidebar.contentDocument.getElementById("bookmarks-view"); + let cellText = tree.view.getCellText( + aElementOrTreeIndex, + tree.columns.getColumnAt(0) + ); + if (!newValue) { + return ( + cellText == + PlacesUIUtils.getBestTitle( + tree.view.nodeForTreeIndex(aElementOrTreeIndex), + true + ) + ); + } + return cellText == newValue; + } + if (!newValue && aElementOrTreeIndex.localName != "toolbarbutton") { + return ( + aElementOrTreeIndex.getAttribute("label") == + PlacesUIUtils.getBestTitle(aElementOrTreeIndex._placesNode) + ); + } + return aElementOrTreeIndex.getAttribute("label") == newValue; + }; + + for (let i = 0; i < views.length; i++) { + let [node, , valid] = searchItemInView(guid, views[i], validator); + Assert.notEqual(node, null, "Should have found the view in " + views[i]); + Assert.equal( + node.title, + newValue, + "Node should have the correct new title" + ); + Assert.ok(valid, "Node element should have the correct label"); + } + }, +}; + +/** + * Search an item guid in a view. + * + * @param {string} itemGuid + * item guid of the item to search. + * @param {string} view + * either "toolbar", "menu" or "sidebar" + * @param {Function} validator + * function to check validity of the found node element. + * @returns {Array} + * [node, index, valid] or [null, null, false] if not found. + */ +function searchItemInView(itemGuid, view, validator) { + switch (view) { + case "toolbar": + return getNodeForToolbarItem(itemGuid, validator); + case "menu": + return getNodeForMenuItem(itemGuid, validator); + case "sidebar": + return getNodeForSidebarItem(itemGuid, validator); + } + + return [null, null, false]; +} + +/** + * Get places node and index for an itemGuid in bookmarks toolbar view. + * + * @param {string} itemGuid + * item guid of the item to search. + * @param {Function} validator + * function to check validity of the found node element. + * @returns {Array} + * [node, index] or [null, null] if not found. + */ +function getNodeForToolbarItem(itemGuid, validator) { + var placesToolbarItems = document.getElementById("PlacesToolbarItems"); + + function findNode(aContainer) { + var children = aContainer.children; + for (var i = 0, staticNodes = 0; i < children.length; i++) { + var child = children[i]; + + // Is this a Places node? + if (!child._placesNode) { + staticNodes++; + continue; + } + + if (child._placesNode.bookmarkGuid == itemGuid) { + let valid = validator ? validator(child) : true; + return [child._placesNode, i - staticNodes, valid]; + } + + // Don't search in queries, they could contain our item in a + // different position. Search only folders + if (PlacesUtils.nodeIsFolder(child._placesNode)) { + var popup = child.menupopup; + popup.openPopup(); + var foundNode = findNode(popup); + popup.hidePopup(); + if (foundNode[0] != null) { + return foundNode; + } + } + } + return [null, null]; + } + + return findNode(placesToolbarItems); +} + +/** + * Get places node and index for an itemGuid in bookmarks menu view. + * + * @param {string} itemGuid + * item guid of the item to search. + * @param {Function} validator + * function to check validity of the found node element. + * @returns {Array} + * [node, index] or [null, null] if not found. + */ +function getNodeForMenuItem(itemGuid, validator) { + var menu = document.getElementById("bookmarksMenu"); + + function findNode(aContainer) { + var children = aContainer.children; + for (var i = 0, staticNodes = 0; i < children.length; i++) { + var child = children[i]; + + // Is this a Places node? + if (!child._placesNode) { + staticNodes++; + continue; + } + + if (child._placesNode.bookmarkGuid == itemGuid) { + let valid = validator ? validator(child) : true; + return [child._placesNode, i - staticNodes, valid]; + } + + // Don't search in queries, they could contain our item in a + // different position. Search only folders + if (PlacesUtils.nodeIsFolder(child._placesNode)) { + var popup = child.lastElementChild; + fakeOpenPopup(popup); + var foundNode = findNode(popup); + + child.open = false; + if (foundNode[0] != null) { + return foundNode; + } + } + } + return [null, null, false]; + } + + return findNode(menu.lastElementChild); +} + +/** + * Get places node and index for an itemGuid in sidebar tree view. + * + * @param {string} itemGuid + * item guid of the item to search. + * @param {Function} validator + * function to check validity of the found node element. + * @returns {Array} + * [node, index] or [null, null] if not found. + */ +function getNodeForSidebarItem(itemGuid, validator) { + var sidebar = document.getElementById("sidebar"); + var tree = sidebar.contentDocument.getElementById("bookmarks-view"); + + function findNode(aContainerIndex) { + if (tree.view.isContainerEmpty(aContainerIndex)) { + return [null, null, false]; + } + + // The rowCount limit is just for sanity, but we will end looping when + // we have checked the last child of this container or we have found node. + for (var i = aContainerIndex + 1; i < tree.view.rowCount; i++) { + var node = tree.view.nodeForTreeIndex(i); + + if (node.bookmarkGuid == itemGuid) { + // Minus one because we want relative index inside the container. + let valid = validator ? validator(i) : true; + return [node, i - tree.view.getParentIndex(i) - 1, valid]; + } + + if (PlacesUtils.nodeIsFolder(node)) { + // Open container. + tree.view.toggleOpenState(i); + // Search inside it. + var foundNode = findNode(i); + // Close container. + tree.view.toggleOpenState(i); + // Return node if found. + if (foundNode[0] != null) { + return foundNode; + } + } + + // We have finished walking this container. + if (!tree.view.hasNextSibling(aContainerIndex + 1, i)) { + break; + } + } + return [null, null, false]; + } + + // Root node is hidden, so we need to manually walk the first level. + for (var i = 0; i < tree.view.rowCount; i++) { + // Open container. + tree.view.toggleOpenState(i); + // Search inside it. + var foundNode = findNode(i); + // Close container. + tree.view.toggleOpenState(i); + // Return node if found. + if (foundNode[0] != null) { + return foundNode; + } + } + return [null, null, false]; +} + +/** + * Get views affected by changes to a folder. + * + * @param {string} folderGuid + * item guid of the folder we have changed. + * @returns {Array<"toolbar" | "menu" | "sidebar">} + * subset of views: ["toolbar", "menu", "sidebar"] + */ +async function getViewsForFolder(folderGuid) { + let rootGuid = folderGuid; + while (!PlacesUtils.isRootItem(rootGuid)) { + let itemData = await PlacesUtils.bookmarks.fetch(rootGuid); + rootGuid = itemData.parentGuid; + } + + switch (rootGuid) { + case PlacesUtils.bookmarks.toolbarGuid: + return ["toolbar", "sidebar"]; + case PlacesUtils.bookmarks.menuGuid: + return ["menu", "sidebar"]; + case PlacesUtils.bookmarks.unfiledGuid: + return ["sidebar"]; + } + return []; +} diff --git a/browser/components/places/tests/browser/favicon-normal16.png b/browser/components/places/tests/browser/favicon-normal16.png Binary files differnew file mode 100644 index 0000000000..62b69a3d03 --- /dev/null +++ b/browser/components/places/tests/browser/favicon-normal16.png diff --git a/browser/components/places/tests/browser/frameLeft.html b/browser/components/places/tests/browser/frameLeft.html new file mode 100644 index 0000000000..5a54fe353b --- /dev/null +++ b/browser/components/places/tests/browser/frameLeft.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>Left frame</title> + </head> + <body> + <a id="clickme" href="frameRight.html" target="right">Open page in the right frame.</a> + </body> +</html> diff --git a/browser/components/places/tests/browser/frameRight.html b/browser/components/places/tests/browser/frameRight.html new file mode 100644 index 0000000000..226accc349 --- /dev/null +++ b/browser/components/places/tests/browser/frameRight.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>Right Frame</title> + </head> + <body> + This is the right frame. + </body> +</html> diff --git a/browser/components/places/tests/browser/framedPage.html b/browser/components/places/tests/browser/framedPage.html new file mode 100644 index 0000000000..58c5bbd79e --- /dev/null +++ b/browser/components/places/tests/browser/framedPage.html @@ -0,0 +1,9 @@ +<html> + <head> + <title>Framed page</title> + </head> + <frameset cols="*,*"> + <frame id="left" name="left" src="frameLeft.html"> + <frame id="right" name="right" src="about:mozilla"> + </frameset> +</html> diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js new file mode 100644 index 0000000000..21790d54aa --- /dev/null +++ b/browser/components/places/tests/browser/head.js @@ -0,0 +1,570 @@ +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "gFluentStrings", function () { + return new Localization(["branding/brand.ftl", "browser/browser.ftl"], true); +}); + +function openLibrary(callback, aLeftPaneRoot) { + let library = window.openDialog( + "chrome://browser/content/places/places.xhtml", + "", + "chrome,toolbar=yes,dialog=no,resizable", + aLeftPaneRoot + ); + waitForFocus(function () { + checkLibraryPaneVisibility(library, aLeftPaneRoot); + callback(library); + }, library); + + return library; +} + +/** + * Returns a handle to a Library window. + * If one is opens returns itm otherwise it opens a new one. + * + * @param {object} aLeftPaneRoot + * Hierarchy to open and select in the left pane. + * @returns {Promise} + * Resolves to the handle to the library window. + */ +function promiseLibrary(aLeftPaneRoot) { + return new Promise(resolve => { + let library = Services.wm.getMostRecentWindow("Places:Organizer"); + if (library && !library.closed) { + if (aLeftPaneRoot) { + library.PlacesOrganizer.selectLeftPaneContainerByHierarchy( + aLeftPaneRoot + ); + } + checkLibraryPaneVisibility(library, aLeftPaneRoot); + resolve(library); + } else { + openLibrary(resolve, aLeftPaneRoot); + } + }); +} + +function promiseLibraryClosed(organizer) { + return new Promise(resolve => { + if (organizer.closed) { + resolve(); + return; + } + // Wait for the Organizer window to actually be closed + organizer.addEventListener( + "unload", + function () { + executeSoon(resolve); + }, + { once: true } + ); + + // Close Library window. + organizer.close(); + }); +} + +function checkLibraryPaneVisibility(library, selectedPane) { + // Make sure right view is shown + if (selectedPane == "Downloads") { + Assert.ok( + library.ContentTree.view.hidden, + "Bookmark/History tree is hidden" + ); + Assert.ok( + !library.document.getElementById("downloadsListBox").hidden, + "Downloads are shown" + ); + } else { + Assert.ok( + !library.ContentTree.view.hidden, + "Bookmark/History tree is shown" + ); + Assert.ok( + library.document.getElementById("downloadsListBox").hidden, + "Downloads are hidden" + ); + } + + // Check currentView getter + Assert.ok(!library.ContentArea.currentView.hidden, "Current view is shown"); +} + +/** + * Waits for a clipboard operation to complete, looking for the expected type. + * + * @see waitForClipboard + * + * @param {Function} aPopulateClipboardFn + * Function to populate the clipboard. + * @param {string} aFlavor + * Data flavor to expect. + * @returns {Promise} + * A promise that is resolved with the data. + */ +function promiseClipboard(aPopulateClipboardFn, aFlavor) { + return new Promise((resolve, reject) => { + waitForClipboard( + data => !!data, + aPopulateClipboardFn, + resolve, + reject, + aFlavor + ); + }); +} + +function synthesizeClickOnSelectedTreeCell(aTree, aOptions) { + if (aTree.view.selection.count < 1) { + throw new Error("The test node should be successfully selected"); + } + // Get selection rowID. + let min = {}, + max = {}; + aTree.view.selection.getRangeAt(0, min, max); + let rowID = min.value; + aTree.ensureRowIsVisible(rowID); + // Calculate the click coordinates. + var rect = aTree.getCoordsForCellItem(rowID, aTree.columns[0], "text"); + var x = rect.x + rect.width / 2; + var y = rect.y + rect.height / 2; + if (aTree.id == "bookmarks-view" || aTree.id == "historyTree") { + // We are purposefully keeping the main <tree> element unlabeled, because in + // this specific case, the on-screen label for either "Bookmarks" or + // "History" sidebar is positioned closely to the tree, visually and in DOM. + // We want to avoid making a screen reader user to listen to a redundant + // announcement, therefore no accessible name is provided to the container + // and we account for this in a11y-checks: + AccessibilityUtils.setEnv({ + labelRule: false, + }); + } + // Simulate the click. + EventUtils.synthesizeMouse( + aTree.body, + x, + y, + aOptions || {}, + aTree.ownerGlobal + ); + AccessibilityUtils.resetEnv(); +} + +/** + * Makes the specified toolbar visible or invisible and returns a Promise object + * that is resolved when the toolbar has completed any animations associated + * with hiding or showing the toolbar. + * + * Note that this code assumes that changes to a toolbar's visibility trigger + * a transition on the max-height property of the toolbar element. + * Changes to this styling could cause the returned Promise object to be + * resolved too early or not at all. + * + * @param {object} aToolbar + * The toolbar to update. + * @param {boolean} aVisible + * True to make the toolbar visible, false to make it hidden. + * + * @returns {Promise} Any animation associated with updating the toolbar's + * visibility has finished. + */ +function promiseSetToolbarVisibility(aToolbar, aVisible) { + if (isToolbarVisible(aToolbar) != aVisible) { + let visibilityChanged = TestUtils.waitForCondition( + () => aToolbar.collapsed != aVisible + ); + setToolbarVisibility(aToolbar, aVisible, undefined, false); + return visibilityChanged; + } + return Promise.resolve(); +} + +/** + * Helper function to determine if the given toolbar is in the visible + * state according to its autohide/collapsed attribute. + * + * @param {object} aToolbar The toolbar to query. + * + * @returns {boolean} True if the relevant attribute on |aToolbar| indicates it is + * visible, false otherwise. + */ +function isToolbarVisible(aToolbar) { + let hidingAttribute = + aToolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; + let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase(); + // Check for both collapsed="true" and collapsed="collapsed" + return hidingValue !== "true" && hidingValue !== hidingAttribute; +} + +/** + * Executes a task after opening the bookmarks dialog, then cancels the dialog. + * + * @param {boolean} autoCancel + * whether to automatically cancel the dialog at the end of the task + * @param {Function} openFn + * generator function causing the dialog to open + * @param {Function} taskFn + * the task to execute once the dialog is open + * @param {Function} closeFn + * A function to be used to wait for pending work when the dialog is + * closing. It is passed the dialog window handle and should return a promise. + * @returns {string} guid + * Bookmark guid + */ +var withBookmarksDialog = async function (autoCancel, openFn, taskFn, closeFn) { + let dialogUrl = "chrome://browser/content/places/bookmarkProperties.xhtml"; + let closed = false; + // We can't show the in-window prompt for windows which don't have + // gDialogBox, like the library (Places:Organizer) window. + let hasDialogBox = !!Services.wm.getMostRecentWindow("").gDialogBox; + let dialogPromise; + if (hasDialogBox) { + dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(null, dialogUrl, { + isSubDialog: true, + }); + } else { + dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null, win => { + return win.document.documentURI.startsWith(dialogUrl); + }).then(win => { + ok( + win.location.href.startsWith(dialogUrl), + "The bookmark properties dialog is open: " + win.location.href + ); + // This is needed for the overlay. + return SimpleTest.promiseFocus(win).then(() => win); + }); + } + let dialogClosePromise = dialogPromise.then(win => { + if (!hasDialogBox) { + return BrowserTestUtils.domWindowClosed(win); + } + let container = win.top.document.getElementById("window-modal-dialog"); + return BrowserTestUtils.waitForEvent(container, "close").then(() => { + return BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); + }); + }); + dialogClosePromise.then(() => { + closed = true; + }); + + info("withBookmarksDialog: opening the dialog"); + // The dialog might be modal and could block our events loop, so executeSoon. + executeSoon(openFn); + + info("withBookmarksDialog: waiting for the dialog"); + let dialogWin = await dialogPromise; + + // Ensure overlay is loaded + info("waiting for the overlay to be loaded"); + await dialogWin.document.mozSubdialogReady; + + // Check the first input is focused. + let doc = dialogWin.document; + let elt = doc.querySelector('input:not([hidden="true"])'); + ok(elt, "There should be an input to focus."); + + if (elt) { + info("waiting for focus on the first textfield"); + await TestUtils.waitForCondition( + () => doc.activeElement == elt, + "The first non collapsed input should have been focused" + ); + } + + info("withBookmarksDialog: executing the task"); + + let closePromise = () => Promise.resolve(); + if (closeFn) { + closePromise = closeFn(dialogWin); + } + let guid; + try { + await taskFn(dialogWin); + } finally { + if (!closed && autoCancel) { + info("withBookmarksDialog: canceling the dialog"); + doc.getElementById("bookmarkpropertiesdialog").cancelDialog(); + await closePromise; + } + guid = await PlacesUIUtils.lastBookmarkDialogDeferred.promise; + // Give the dialog a little time to close itself. + await dialogClosePromise; + } + return guid; +}; + +/** + * Opens the contextual menu on the element pointed by the given selector. + * + * @param {object} browser + * The associated browser element. + * @param {object} selector + * Valid selector syntax + * @returns {Promise} + * Returns a Promise that resolves once the context menu has been + * opened. + */ +var openContextMenuForContentSelector = async function (browser, selector) { + info("wait for the context menu"); + let contextPromise = BrowserTestUtils.waitForEvent( + document.getElementById("contentAreaContextMenu"), + "popupshown" + ); + await SpecialPowers.spawn(browser, [{ selector }], async function (args) { + let doc = content.document; + let elt = doc.querySelector(args.selector); + dump(`openContextMenuForContentSelector: found ${elt}\n`); + + /* Open context menu so chrome can access the element */ + const domWindowUtils = content.windowUtils; + let rect = elt.getBoundingClientRect(); + let left = rect.left + rect.width / 2; + let top = rect.top + rect.height / 2; + domWindowUtils.sendMouseEvent( + "contextmenu", + left, + top, + 2, + 1, + 0, + false, + 0, + 0, + true + ); + }); + await contextPromise; +}; + +/** + * Fills a bookmarks dialog text field ensuring to cause expected edit events. + * + * @param {string} id + * id of the text field + * @param {string} text + * text to fill in + * @param {object} win + * dialog window + * @param {boolean} [blur] + * whether to blur at the end. + */ +function fillBookmarkTextField(id, text, win, blur = true) { + let elt = win.document.getElementById(id); + elt.focus(); + elt.select(); + if (!text) { + EventUtils.synthesizeKey("VK_DELETE", {}, win); + } else { + for (let c of text.split("")) { + EventUtils.synthesizeKey(c, {}, win); + } + } + if (blur) { + elt.blur(); + } +} + +/** + * Executes a task after opening the bookmarks or history sidebar. Takes care + * of closing the sidebar once done. + * + * @param {string} type + * either "bookmarks" or "history". + * @param {Function} taskFn + * The task to execute once the sidebar is ready. Will get the Places + * tree view as input. + */ +var withSidebarTree = async function (type, taskFn) { + let sidebar = document.getElementById("sidebar"); + info("withSidebarTree: waiting sidebar load"); + let sidebarLoadedPromise = new Promise(resolve => { + sidebar.addEventListener( + "load", + function () { + executeSoon(resolve); + }, + { capture: true, once: true } + ); + }); + let sidebarId = + type == "bookmarks" ? "viewBookmarksSidebar" : "viewHistorySidebar"; + SidebarUI.show(sidebarId); + await sidebarLoadedPromise; + + let treeId = type == "bookmarks" ? "bookmarks-view" : "historyTree"; + let tree = sidebar.contentDocument.getElementById(treeId); + + // Need to executeSoon since the tree is initialized on sidebar load. + info("withSidebarTree: executing the task"); + try { + await taskFn(tree); + } finally { + SidebarUI.hide(); + } +}; + +/** + * Executes a task after opening the Library on a given root. Takes care + * of closing the library once done. + * + * @param {string} hierarchy + * The left pane hierarchy to open. + * @param {Function} taskFn + * The task to execute once the Library is ready. + * Will get { left, right } trees as argument. + */ +var withLibraryWindow = async function (hierarchy, taskFn) { + let library = await promiseLibrary(hierarchy); + let left = library.document.getElementById("placesList"); + let right = library.document.getElementById("placeContent"); + info("withLibrary: executing the task"); + try { + await taskFn({ left, right }); + } finally { + await promiseLibraryClosed(library); + } +}; + +function promisePlacesInitComplete() { + const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver + ); + + let placesInitCompleteObserved = TestUtils.topicObserved( + "places-browser-init-complete" + ); + + gBrowserGlue.observe( + null, + "browser-glue-test", + "places-browser-init-complete" + ); + + return placesInitCompleteObserved; +} + +// Function copied from browser/base/content/test/general/head.js. +function promisePopupShown(popup) { + return new Promise(resolve => { + if (popup.state == "open") { + resolve(); + } else { + let onPopupShown = event => { + popup.removeEventListener("popupshown", onPopupShown); + resolve(); + }; + popup.addEventListener("popupshown", onPopupShown); + } + }); +} + +// Function copied from browser/base/content/test/general/head.js. +function promisePopupHidden(popup) { + return new Promise(resolve => { + let onPopupHidden = event => { + popup.removeEventListener("popuphidden", onPopupHidden); + resolve(); + }; + popup.addEventListener("popuphidden", onPopupHidden); + }); +} + +// Identify a bookmark node in the Bookmarks Toolbar by its guid. +function getToolbarNodeForItemGuid(itemGuid) { + let children = document.getElementById("PlacesToolbarItems").childNodes; + for (let child of children) { + if (itemGuid === child._placesNode.bookmarkGuid) { + return child; + } + } + return null; +} + +// Open the bookmarks Star UI by clicking the star button on the address bar. +async function clickBookmarkStar(win = window) { + let shownPromise = promisePopupShown( + win.document.getElementById("editBookmarkPanel") + ); + win.BookmarkingUI.star.click(); + await shownPromise; + + // Additionally await for the async init to complete. + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + await BrowserTestUtils.waitForMutationCondition( + menuList, + { attributes: true }, + () => !!menuList.getAttribute("selectedGuid"), + "Should select the menu folder item" + ); +} + +// Close the bookmarks Star UI by clicking the "Done" button. +async function hideBookmarksPanel(win = window) { + let hiddenPromise = promisePopupHidden( + win.document.getElementById("editBookmarkPanel") + ); + // Confirm and close the dialog. + win.document.getElementById("editBookmarkPanelDoneButton").click(); + await hiddenPromise; +} + +// Create a temporary folder, set it as the default folder, +// then remove the folder. This is used to ensure that the +// default folder gets reset properly. +async function createAndRemoveDefaultFolder() { + let tempFolder = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "temp folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.bookmarks.defaultLocation", tempFolder[0].guid]], + }); + + let defaultGUID = await PlacesUIUtils.defaultParentGuid; + is(defaultGUID, tempFolder[0].guid, "check default guid"); + + await PlacesUtils.bookmarks.remove(tempFolder); +} + +async function showLibraryColumn(library, columnName) { + const viewMenu = library.document.getElementById("viewMenu"); + const viewMenuPopup = library.document.getElementById("viewMenuPopup"); + const onViewMenuPopup = new Promise(resolve => { + viewMenuPopup.addEventListener("popupshown", () => resolve(), { + once: true, + }); + }); + EventUtils.synthesizeMouseAtCenter(viewMenu, {}, library); + await onViewMenuPopup; + + const viewColumns = library.document.getElementById("viewColumns"); + const viewColumnsPopup = viewColumns.querySelector("menupopup"); + const onViewColumnsPopup = new Promise(resolve => { + viewColumnsPopup.addEventListener("popupshown", () => resolve(), { + once: true, + }); + }); + EventUtils.synthesizeMouseAtCenter(viewColumns, {}, library); + await onViewColumnsPopup; + + const columnMenu = library.document.getElementById(`menucol_${columnName}`); + EventUtils.synthesizeMouseAtCenter(columnMenu, {}, library); +} + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.bookmarks.defaultLocation"); + await PlacesTransactions.clearTransactionsHistory(true, true); +}); diff --git a/browser/components/places/tests/browser/interactions/browser.toml b/browser/components/places/tests/browser/interactions/browser.toml new file mode 100644 index 0000000000..ce39403409 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser.toml @@ -0,0 +1,36 @@ +[DEFAULT] +prefs = [ + "browser.places.interactions.enabled=true", + "browser.places.interactions.log=true", + "browser.places.interactions.scrolling_timeout_ms=50", + "general.smoothScroll=false", +] + +support-files = [ + "head.js", + "../keyword_form.html", + "scrolling.html", + "scrolling_subframe.html", +] + +["browser_interactions_blocklist.js"] + +["browser_interactions_clearHistory.js"] + +["browser_interactions_disabledHistory.js"] + +["browser_interactions_referrer.js"] + +["browser_interactions_scrolling.js"] +skip-if = ["apple_silicon && fission"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + +["browser_interactions_scrolling_dom_history.js"] +skip-if = ["os == 'mac'"] # Bug 1756157: Randomly times out on macOS + +["browser_interactions_typing.js"] + +["browser_interactions_typing_dom_history.js"] + +["browser_interactions_view_time.js"] + +["browser_interactions_view_time_dom_history.js"] diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js new file mode 100644 index 0000000000..e7a880637b --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that interactions are not recorded for sites on the blocklist. + */ + +const ALLOWED_TEST_URL = "http://mochi.test:8888/"; +const BLOCKED_TEST_URL = "https://example.com/browser"; + +ChromeUtils.defineESModuleGetters(this, { + FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs", + InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs", +}); + +add_setup(async function () { + let oldBlocklistValue = Services.prefs.getStringPref( + "places.interactions.customBlocklist", + "[]" + ); + Services.prefs.setStringPref("places.interactions.customBlocklist", "[]"); + + registerCleanupFunction(async () => { + Services.prefs.setStringPref( + "places.interactions.customBlocklist", + oldBlocklistValue + ); + }); +}); +/** + * Loads the blocked URL, then loads about:blank to trigger the end of the + * interaction with the blocked URL. + * + * @param {boolean} expectRecording + * True if we expect the blocked URL to have been recorded in the database. + */ +async function loadBlockedUrl(expectRecording) { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(ALLOWED_TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.startLoadingURIString(browser, BLOCKED_TEST_URL); + await BrowserTestUtils.browserLoaded(browser, false, BLOCKED_TEST_URL); + + await assertDatabaseValues([ + { + url: ALLOWED_TEST_URL, + totalViewTime: 10000, + }, + ]); + + Interactions._pageViewStartTime = Cu.now() - 20000; + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + if (expectRecording) { + await assertDatabaseValues([ + { + url: ALLOWED_TEST_URL, + totalViewTime: 10000, + }, + { + url: BLOCKED_TEST_URL, + totalViewTime: 20000, + }, + ]); + } else { + await assertDatabaseValues([ + { + url: ALLOWED_TEST_URL, + totalViewTime: 10000, + }, + ]); + } + }); +} + +add_task(async function test_regexp() { + info("Record BLOCKED_TEST_URL because it is not yet blocklisted."); + await loadBlockedUrl(true); + + info("Add BLOCKED_TEST_URL to the blocklist and verify it is not recorded."); + let blockedRegex = /^(https?:\/\/)?example\.com\/browser/i; + InteractionsBlocklist.addRegexToBlocklist(blockedRegex); + Assert.equal( + Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"), + JSON.stringify([blockedRegex.toString()]) + ); + await loadBlockedUrl(false); + + info("Remove BLOCKED_TEST_URL from the blocklist and verify it is recorded."); + InteractionsBlocklist.removeRegexFromBlocklist(blockedRegex); + Assert.equal( + Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"), + JSON.stringify([]) + ); + await loadBlockedUrl(true); +}); + +add_task(async function test_adult() { + FilterAdult.addDomainToList("https://example.com/browser"); + await loadBlockedUrl(false); + FilterAdult.removeDomainFromList("https://example.com/browser"); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js b/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js new file mode 100644 index 0000000000..37b99ba974 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests we clear interactions when history is cleared. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html"; +const TEST_URL_2 = + "https://example.org/browser/browser/components/places/tests/browser/keyword_form.html"; +const TEST_URL_AWAY = "https://example.com/browser"; + +const sentence = "The quick brown fox jumps over the lazy dog."; + +async function sendTextToInput(browser, text) { + // Reset to later verify that the provided text matches the value. + await SpecialPowers.spawn(browser, [], function () { + const input = content.document.querySelector( + "#form1 > input[name='search']" + ); + input.focus(); + input.value = ""; + }); + + EventUtils.sendString(text); + + await SpecialPowers.spawn(browser, [{ text }], async function (args) { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("#form1 > input[name='search']").value == + args.text, + "Text has been set on input" + ); + }); +} + +add_setup(async function () { + await Interactions.reset(); + await PlacesUtils.bookmarks.insert({ + url: TEST_URL, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + registerCleanupFunction(async () => { + await Interactions.reset(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_clear_history() { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL_2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_2); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + }); + + Assert.ok( + await PlacesUtils.history.hasVisits(TEST_URL), + "Check visits were added" + ); + Assert.ok( + await PlacesUtils.history.hasVisits(TEST_URL_2), + "Check visits were added" + ); + + info("Check interactions were added"); + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL_2, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + + await PlacesUtils.history.clear(); + + Assert.ok( + await PlacesTestUtils.isPageInDB(TEST_URL), + "Bookmarked page remains in the database" + ); + Assert.ok( + !(await PlacesUtils.history.hasVisits(TEST_URL)), + "Check visits were removed" + ); + Assert.ok( + !(await PlacesTestUtils.isPageInDB(TEST_URL_2)), + "Non bookmarked page was removed from the database" + ); + + info("Check all interactions have been removed."); + await assertDatabaseValues([]); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js b/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js new file mode 100644 index 0000000000..a5a62207cf --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests we don't record interactions when history is disabled, even if the page + * is bookmarked. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html"; +const TEST_URL_AWAY = "https://example.com/browser"; + +const sentence = "The quick brown fox jumps over the lazy dog."; + +async function sendTextToInput(browser, text) { + // Reset to later verify that the provided text matches the value. + await SpecialPowers.spawn(browser, [], function () { + const input = content.document.querySelector( + "#form1 > input[name='search']" + ); + input.focus(); + input.value = ""; + }); + + EventUtils.sendString(text); + + await SpecialPowers.spawn(browser, [{ text }], async function (args) { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("#form1 > input[name='search']").value == + args.text, + "Text has been set on input" + ); + }); +} + +add_setup(async function () { + await Interactions.reset(); + await PlacesUtils.bookmarks.insert({ + url: TEST_URL, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + registerCleanupFunction(async () => { + await Interactions.reset(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_disabled_history() { + await SpecialPowers.pushPrefEnv({ + set: [["places.history.enabled", false]], + }); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL_AWAY); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_AWAY); + + await assertDatabaseValues([]); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([]); + }); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_disableGlobalHistory() { + await Interactions.reset(); + + await PlacesUtils.history.clear(); + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + let browser = tab.linkedBrowser; + browser.setAttribute("disableglobalhistory", "true"); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL); + Assert.ok( + !browser.browsingContext.useGlobalHistory, + "browserContext should be updated after the first load" + ); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL_AWAY); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_AWAY); + + Assert.ok( + !(await PlacesUtils.history.hasVisits(TEST_URL)), + "Check visits were not added" + ); + Assert.ok( + !(await PlacesUtils.history.hasVisits(TEST_URL_AWAY)), + "Check visits were not added" + ); + await assertDatabaseValues([]); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js new file mode 100644 index 0000000000..fda93bfc5a --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests page view time recording for interactions. + */ + +const TEST_REFERRER_URL = "https://example.org/browser"; +const TEST_URL = "https://example.org/browser/browser"; + +add_task(async function test_interactions_referrer() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_REFERRER_URL, async browser => { + let ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ); + + // Load a new URI with a specific referrer. + let referrerInfo1 = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + Services.io.newURI(TEST_REFERRER_URL) + ); + browser.loadURI(Services.io.newURI(TEST_URL), { + referrerInfo: referrerInfo1, + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + {} + ), + }); + await BrowserTestUtils.browserLoaded(browser, true, TEST_URL); + }); + await assertDatabaseValues([ + { + url: TEST_REFERRER_URL, + referrer_url: null, + }, + { + url: TEST_URL, + referrer_url: TEST_REFERRER_URL, + }, + ]); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js new file mode 100644 index 0000000000..59428a485d --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test reporting of scrolling interactions. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html"; +const TEST_URL2 = "https://example.com/browser"; + +async function waitForScrollEvent(aBrowser, aTask) { + let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll"); + + // This forces us to send a message to the browser's process and receive a response which ensures + // that the message sent to register the scroll event listener will also have been processed by + // the content process. Without this it is possible for our scroll task to send a higher priority + // message which can be processed by the content process before the message to register the scroll + // event listener. + await SpecialPowers.spawn(aBrowser, [], () => {}); + + await aTask(); + await promise; +} + +add_task(async function test_no_scrolling() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + exactscrollingDistance: 0, + exactscrollingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_arrow_key_down_scroll() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_scrollIntoView() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await waitForScrollEvent(browser, () => + SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("middleHeading"); + heading.scrollIntoView(); + }) + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + // JS-triggered scrolling should not be reported + await assertDatabaseValues([ + { + url: TEST_URL, + exactscrollingDistance: 0, + exactscrollingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_anchor_click() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await waitForScrollEvent(browser, () => + SpecialPowers.spawn(browser, [], function () { + const anchor = content.document.getElementById("to_bottom_anchor"); + anchor.click(); + }) + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + // The scrolling resulting from clicking on an anchor should not be reported + await assertDatabaseValues([ + { + url: TEST_URL, + exactscrollingDistance: 0, + exactscrollingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_window_scrollBy() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await waitForScrollEvent(browser, () => + SpecialPowers.spawn(browser, [], function () { + content.scrollBy(0, 100); + }) + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + // The scrolling resulting from the window.scrollBy() call should not be reported + await assertDatabaseValues([ + { + url: TEST_URL, + exactscrollingDistance: 0, + exactscrollingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_window_scrollTo() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await waitForScrollEvent(browser, () => + SpecialPowers.spawn(browser, [], function () { + content.scrollTo(0, 200); + }) + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + // The scrolling resulting from the window.scrollTo() call should not be reported + await assertDatabaseValues([ + { + url: TEST_URL, + exactscrollingDistance: 0, + exactscrollingTime: 0, + }, + ]); + }); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js new file mode 100644 index 0000000000..b825ce615e --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test reporting of scrolling interactions after DOM history API use. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html"; +const TEST_URL2 = "https://example.com/browser"; + +async function waitForScrollEvent(aBrowser, aTask) { + let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll"); + + // This forces us to send a message to the browser's process and receive a response which ensures + // that the message sent to register the scroll event listener will also have been processed by + // the content process. Without this it is possible for our scroll task to send a higher priority + // message which can be processed by the content process before the message to register the scroll + // event listener. + await SpecialPowers.spawn(aBrowser, [], () => {}); + + await aTask(); + await promise; +} + +add_task(async function test_scroll_pushState() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.pushState(null, "", url); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL2, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_scroll_pushState_sameUrl() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.pushState(null, "", url); + }); + + // As the page hasn't changed there will be no interactions saved yet. + await assertDatabaseValues([]); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_scroll_replaceState() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.replaceState(null, "", url); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL2, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_scroll_replaceState_sameUrl() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.replaceState(null, "", url); + }); + + // As the page hasn't changed there will be no interactions saved yet. + await assertDatabaseValues([]); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_scroll_hashchange() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await SpecialPowers.spawn(browser, [], function () { + const heading = content.document.getElementById("heading"); + heading.focus(); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + await ContentTask.spawn(browser, TEST_URL + "#foo", url => { + content.history.replaceState(null, "", url); + }); + + await waitForScrollEvent(browser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + scrollingDistanceIsGreaterThan: 0, + scrollingTimeIsGreaterThan: 0, + }, + ]); + }); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js new file mode 100644 index 0000000000..99269c3265 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js @@ -0,0 +1,410 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests reporting of typing interactions. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html"; +const TEST_URL2 = "https://example.com/browser"; +const TEST_URL3 = + "https://example.com/browser/browser/base/content/test/contextMenu/subtst_contextmenu_input.html"; + +const sentence = "The quick brown fox jumps over the lazy dog."; +const sentenceFragments = [ + "The quick", + " brown fox", + " jumps over the lazy dog.", +]; +const longSentence = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut purus a libero cursus scelerisque. In hac habitasse platea dictumst. Quisque posuere ante sed consequat volutpat."; + +// For tests where it matters reduces the maximum time between keypresses to a length that we can +// afford to delay the test by. +const PREF_TYPING_DELAY = "browser.places.interactions.typing_timeout_ms"; +const POST_TYPING_DELAY = 150; + +async function sendTextToInput(browser, text) { + await SpecialPowers.spawn(browser, [], function () { + const input = content.document.querySelector( + "#form1 > input[name='search']" + ); + input.focus(); + input.value = ""; // Reset to later verify that the provided text matches the value + }); + + EventUtils.sendString(text); + + await SpecialPowers.spawn(browser, [{ text }], async function (args) { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("#form1 > input[name='search']").value == + args.text, + "Text has been set on input" + ); + }); +} + +add_task(async function test_load_and_navigate_away_no_keypresses() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + { + url: TEST_URL2, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_load_type_and_navigate_away() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL2, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + }); +}); + +add_task(async function test_no_typing_close_tab() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => {}); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + ]); +}); + +add_task(async function test_typing_close_tab() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); +}); + +add_task(async function test_single_key_typing_and_delay() { + await Interactions.reset(); + + Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY); + + try { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + // Single keystrokes with a delay between each, are not considered typing + const text = ["T", "h", "e"]; + + for (let i = 0; i < text.length; i++) { + await sendTextToInput(browser, text[i]); + + // We do need to wait here because typing is defined as a series of keystrokes followed by a delay. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2)); + } + }); + } finally { + Services.prefs.clearUserPref(PREF_TYPING_DELAY); + } + + // Since we typed single keys with delays between each, there should be no typing added to the database + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + ]); +}); + +add_task(async function test_double_key_typing_and_delay() { + await Interactions.reset(); + + // Test three 2-key typing bursts. + const text = ["Ab", "cd", "ef"]; + + const testStartTime = Cu.now(); + + Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY); + + try { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + for (let i = 0; i < text.length; i++) { + await sendTextToInput(browser, text[i]); + + // We do need to wait here because typing is defined as a series of keystrokes followed by a delay. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2)); + } + }); + } finally { + Services.prefs.clearUserPref(PREF_TYPING_DELAY); + } + + // All keys should be recorded + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: text.reduce( + (accumulator, current) => accumulator + current.length, + 0 + ), + typingTimeIsLessThan: Cu.now() - testStartTime, + }, + ]); +}); + +add_task(async function test_typing_and_delay() { + await Interactions.reset(); + + const testStartTime = Cu.now(); + + Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY); + + try { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + for (let i = 0; i < sentenceFragments.length; i++) { + await sendTextToInput(browser, sentenceFragments[i]); + + // We do need to wait here because typing is defined as a series of keystrokes followed by a delay. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2)); + } + }); + } finally { + Services.prefs.clearUserPref(PREF_TYPING_DELAY); + } + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentenceFragments.reduce( + (accumulator, current) => accumulator + current.length, + 0 + ), + typingTimeIsGreaterThan: 0, + typingTimeIsLessThan: Cu.now() - testStartTime, + }, + ]); +}); + +add_task(async function test_typing_and_reload() { + await Interactions.reset(); + + const testStartTime = Cu.now(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentenceFragments[0]); + + info("reload"); + browser.reload(); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL); + + // First typing should have been recorded + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentenceFragments[0].length, + typingTimeIsGreaterThan: 0, + }, + ]); + + await sendTextToInput(browser, sentenceFragments[1]); + + info("reload"); + browser.reload(); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL); + + // Second typing should have been recorded + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentenceFragments[0].length, + typingTimeIsGreaterThan: 0, + typingTimeIsLessThan: Cu.now() - testStartTime, + }, + { + url: TEST_URL, + keypresses: sentenceFragments[1].length, + typingTimeIsGreaterThan: 0, + typingTimeIsLessThan: Cu.now() - testStartTime, + }, + ]); + }); +}).skip(); // Bug 1749328 - intermittent failure: dropping the 2nd interaction after the 2nd reload + +add_task(async function test_switch_tabs_no_typing() { + await Interactions.reset(); + + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2); + + info("Switch to second tab"); + gBrowser.selectedTab = tab2; + + // Only the interaction of the first tab should be recorded so far, and with no typing + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function test_typing_switch_tabs() { + await Interactions.reset(); + + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + await sendTextToInput(tab1.linkedBrowser, sentence); + + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL3); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL3); + + info("Switch to second tab"); + await BrowserTestUtils.switchTab(gBrowser, tab2); + + // Only the interaction of the first tab should be recorded so far + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + + const tab1TyingTime = await getDatabaseValue(TEST_URL, "typingTime"); + + info("Switch back to first tab"); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + // The interaction of the second tab should now be recorded (no typing) + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + exactTypingTime: tab1TyingTime, + }, + { + url: TEST_URL3, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + + info("Switch back to the second tab"); + await BrowserTestUtils.switchTab(gBrowser, tab2); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + exactTypingTime: tab1TyingTime, + }, + { + url: TEST_URL3, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + + // Typing into the second tab + await SpecialPowers.spawn(tab2.linkedBrowser, [], function () { + const input = content.document.getElementById("input_text"); + input.focus(); + }); + await EventUtils.sendString(longSentence); + await TestUtils.waitForTick(); + + info("Switch back to first tab"); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + // The interaction of the second tab should now also be recorded (with typing) + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + exactTypingTime: tab1TyingTime, + }, + { + url: TEST_URL3, + keypresses: longSentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js new file mode 100644 index 0000000000..e64050ec1d --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests reporting of typing interactions after DOM history API usage. + */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html"; +const TEST_URL2 = "https://example.com/browser"; + +const sentence = "The quick brown fox jumps over the lazy dog."; + +async function sendTextToInput(browser, text) { + await SpecialPowers.spawn(browser, [], function () { + const input = content.document.querySelector( + "#form1 > input[name='search']" + ); + input.focus(); + input.value = ""; // Reset to later verify that the provided text matches the value + }); + + EventUtils.sendString(text); + + await SpecialPowers.spawn(browser, [{ text }], async function (args) { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("#form1 > input[name='search']").value == + args.text, + "Text has been set on input" + ); + }); +} + +add_task(async function test_typing_pushState() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.pushState(null, "", url); + }); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL2, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_typing_pushState_sameUrl() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.pushState(null, "", url); + }); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length * 2, + typingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_typing_replaceState() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.replaceState(null, "", url); + }); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + { + url: TEST_URL2, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_typing_replaceState_sameUrl() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.replaceState(null, "", url); + }); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length * 2, + typingTimeIsGreaterThan: 0, + }, + ]); + }); +}); + +add_task(async function test_typing_hashchange() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + await sendTextToInput(browser, sentence); + + await ContentTask.spawn(browser, TEST_URL + "#foo", url => { + content.location = url; + }); + + await sendTextToInput(browser, sentence); + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length * 2, + typingTimeIsGreaterThan: 0, + }, + ]); + }); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js new file mode 100644 index 0000000000..278ae10228 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js @@ -0,0 +1,410 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests page view time recording for interactions. + */ + +const TEST_URL = "https://example.com/"; +const TEST_URL2 = "https://example.com/browser"; +const TEST_URL3 = "https://example.com/browser/browser"; +const TEST_URL4 = "https://example.com/browser/browser/components"; + +add_task(async function test_interactions_simple_load_and_navigate_away() { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.startLoadingURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + + Interactions._pageViewStartTime = Cu.now() - 20000; + + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); + }); +}); + +add_task(async function test_interactions_simple_load_and_change_to_non_http() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.startLoadingURIString(browser, "about:support"); + await BrowserTestUtils.browserLoaded(browser, false, "about:support"); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + }); +}); + +add_task(async function test_interactions_close_tab() { + await Interactions.reset(); + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 20000, + }, + ]); +}); + +add_task(async function test_interactions_background_tab() { + await Interactions.reset(); + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2); + + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.removeTab(tab2); + + // This is checking a non-action, so let the event queue clear to try and + // detect any unexpected database writes. We wait for a few ticks to + // make it more likely. however if this fails it may show up as an + // intermittent. + await TestUtils.waitForTick(); + await TestUtils.waitForTick(); + await TestUtils.waitForTick(); + + await assertDatabaseValues([]); + + BrowserTestUtils.removeTab(tab1); + + // Only the interaction in the visible tab should have been recorded. + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); +}); + +add_task(async function test_interactions_switch_tabs() { + await Interactions.reset(); + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2); + + info("Switch to second tab"); + Interactions._pageViewStartTime = Cu.now() - 10000; + gBrowser.selectedTab = tab2; + + // Only the interaction of the first tab should be recorded so far. + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + let tab1ViewTime = await getDatabaseValue(TEST_URL, "totalViewTime"); + + info("Switch back to first tab"); + Interactions._pageViewStartTime = Cu.now() - 20000; + gBrowser.selectedTab = tab1; + + // The interaction of the second tab should now be recorded. + await assertDatabaseValues([ + { + url: TEST_URL, + exactTotalViewTime: tab1ViewTime, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); + + info("Switch to second tab again"); + Interactions._pageViewStartTime = Cu.now() - 30000; + gBrowser.selectedTab = tab2; + + // The interaction of the second tab should now be recorded. + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: tab1ViewTime + 30000, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function test_interactions_switch_windows() { + await Interactions.reset(); + + // Open a tab in the first window. + let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + // and then load the second window. + Interactions._pageViewStartTime = Cu.now() - 10000; + + let otherWin = await BrowserTestUtils.openNewBrowserWindow(); + + BrowserTestUtils.startLoadingURIString( + otherWin.gBrowser.selectedBrowser, + TEST_URL2 + ); + await BrowserTestUtils.browserLoaded( + otherWin.gBrowser.selectedBrowser, + false, + TEST_URL2 + ); + await SimpleTest.promiseFocus(otherWin); + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + let originalWindowViewTime = await getDatabaseValue( + TEST_URL, + "totalViewTime" + ); + + info("Switch back to original window"); + Interactions._pageViewStartTime = Cu.now() - 20000; + await SimpleTest.promiseFocus(window); + await assertDatabaseValues([ + { + url: TEST_URL, + exactTotalViewTime: originalWindowViewTime, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); + let newWindowViewTime = await getDatabaseValue(TEST_URL2, "totalViewTime"); + + info("Switch back to new window"); + Interactions._pageViewStartTime = Cu.now() - 30000; + await SimpleTest.promiseFocus(otherWin); + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: originalWindowViewTime + 30000, + }, + { + url: TEST_URL2, + exactTotalViewTime: newWindowViewTime, + }, + ]); + + BrowserTestUtils.removeTab(tabInOriginalWindow); + await BrowserTestUtils.closeWindow(otherWin); +}); + +add_task(async function test_interactions_loading_in_unfocused_windows() { + await Interactions.reset(); + + let otherWin = await BrowserTestUtils.openNewBrowserWindow(); + + BrowserTestUtils.startLoadingURIString( + otherWin.gBrowser.selectedBrowser, + TEST_URL + ); + await BrowserTestUtils.browserLoaded( + otherWin.gBrowser.selectedBrowser, + false, + TEST_URL + ); + + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.startLoadingURIString( + otherWin.gBrowser.selectedBrowser, + TEST_URL2 + ); + await BrowserTestUtils.browserLoaded( + otherWin.gBrowser.selectedBrowser, + false, + TEST_URL2 + ); + + // Only the interaction of the first tab should be recorded so far. + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + let newWindowViewTime = await getDatabaseValue(TEST_URL, "totalViewTime"); + + // Open a tab in the background window, and then navigate somewhere else, + // this should not record an intereaction. + let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL3, + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + + BrowserTestUtils.startLoadingURIString( + tabInOriginalWindow.linkedBrowser, + TEST_URL4 + ); + await BrowserTestUtils.browserLoaded( + tabInOriginalWindow.linkedBrowser, + false, + TEST_URL4 + ); + + // Only the interaction of the first tab should be recorded so far. + await assertDatabaseValues([ + { + url: TEST_URL, + exactTotalViewTime: newWindowViewTime, + }, + ]); + + BrowserTestUtils.removeTab(tabInOriginalWindow); + await BrowserTestUtils.closeWindow(otherWin); +}); + +add_task(async function test_interactions_private_browsing() { + await Interactions.reset(); + + // Open a tab in the first window. + let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + // and then load the second window. + Interactions._pageViewStartTime = Cu.now() - 10000; + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + BrowserTestUtils.startLoadingURIString( + privateWin.gBrowser.selectedBrowser, + TEST_URL2 + ); + await BrowserTestUtils.browserLoaded( + privateWin.gBrowser.selectedBrowser, + false, + TEST_URL2 + ); + await SimpleTest.promiseFocus(privateWin); + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + let originalWindowViewTime = await getDatabaseValue( + TEST_URL, + "totalViewTime" + ); + + info("Switch back to original window"); + Interactions._pageViewStartTime = Cu.now() - 20000; + // As we're checking for a non-action, wait for the focus to have definitely + // completed, and then let the event queues clear. + await SimpleTest.promiseFocus(window); + await TestUtils.waitForTick(); + + // The private window site should not be recorded. + await assertDatabaseValues([ + { + url: TEST_URL, + exactTotalViewTime: originalWindowViewTime, + }, + ]); + + info("Switch back to new window"); + Interactions._pageViewStartTime = Cu.now() - 30000; + await SimpleTest.promiseFocus(privateWin); + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: originalWindowViewTime + 30000, + }, + ]); + + BrowserTestUtils.removeTab(tabInOriginalWindow); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function test_interactions_idle() { + await Interactions.reset(); + let lastViewTime; + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + Interactions.observe(null, "idle", ""); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + lastViewTime = await getDatabaseValue(TEST_URL, "totalViewTime"); + + Interactions._pageViewStartTime = Cu.now() - 20000; + + Interactions.observe(null, "active", ""); + + await assertDatabaseValues([ + { + url: TEST_URL, + exactTotalViewTime: lastViewTime, + }, + ]); + + Interactions._pageViewStartTime = Cu.now() - 30000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: lastViewTime + 30000, + maxViewTime: lastViewTime + 30000 + 10000, + }, + ]); +}); diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js new file mode 100644 index 0000000000..857a846417 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests page view time recording for interactions after DOM history API usage. + */ + +const TEST_URL = "https://example.com/"; +const TEST_URL2 = "https://example.com/browser"; + +add_task(async function test_interactions_pushState() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.pushState(null, "", url); + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); +}); + +add_task(async function test_interactions_pushState_sameUrl() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.pushState(null, "", url); + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 20000, + }, + ]); +}); + +add_task(async function test_interactions_replaceState() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + await ContentTask.spawn(browser, TEST_URL2, url => { + content.history.replaceState(null, "", url); + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + { + url: TEST_URL2, + totalViewTime: 20000, + }, + ]); +}); + +add_task(async function test_interactions_replaceState_sameUrl() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + await ContentTask.spawn(browser, TEST_URL, url => { + content.history.replaceState(null, "", url); + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 20000, + }, + ]); +}); + +add_task(async function test_interactions_hashchange() { + await Interactions.reset(); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + Interactions._pageViewStartTime = Cu.now() - 10000; + + await ContentTask.spawn(browser, TEST_URL + "#foo", url => { + content.location = url; + }); + + Interactions._pageViewStartTime = Cu.now() - 20000; + }); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 20000, + }, + ]); +}); diff --git a/browser/components/places/tests/browser/interactions/head.js b/browser/components/places/tests/browser/interactions/head.js new file mode 100644 index 0000000000..92a096cc5f --- /dev/null +++ b/browser/components/places/tests/browser/interactions/head.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Interactions } = ChromeUtils.importESModule( + "resource:///modules/Interactions.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "pageViewIdleTime", + "browser.places.interactions.pageViewIdleTime", + 60 +); + +add_setup(async function global_setup() { + // Disable idle management because it interacts with our code, causing + // unexpected intermittent failures, we'll fake idle notifications when + // we need to test it. + let idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService( + Ci.nsIUserIdleService + ); + idleService.removeIdleObserver(Interactions, pageViewIdleTime); + registerCleanupFunction(() => { + idleService.addIdleObserver(Interactions, pageViewIdleTime); + }); + + // Clean interactions for each test. + await Interactions.reset(); + registerCleanupFunction(async () => { + await Interactions.reset(); + }); +}); + +/** + * Ensures that a list of interactions have been permanently stored. + * + * @param {Array} expected list of interactions to be found. + * @param {boolean} [dontFlush] Avoid flushing pending data. + */ +async function assertDatabaseValues(expected, { dontFlush = false } = {}) { + await Interactions.interactionUpdatePromise; + if (!dontFlush) { + await Interactions.store.flush(); + } + + let interactions = await PlacesUtils.withConnectionWrapper( + "head.js::assertDatabaseValues", + async db => { + let rows = await db.execute(` + SELECT h.url AS url, h2.url as referrer_url, total_view_time, key_presses, typing_time, scrolling_time, scrolling_distance + FROM moz_places_metadata m + JOIN moz_places h ON h.id = m.place_id + LEFT JOIN moz_places h2 ON h2.id = m.referrer_place_id + ORDER BY created_at ASC + `); + return rows.map(r => ({ + url: r.getResultByName("url"), + referrerUrl: r.getResultByName("referrer_url"), + keypresses: r.getResultByName("key_presses"), + typingTime: r.getResultByName("typing_time"), + totalViewTime: r.getResultByName("total_view_time"), + scrollingTime: r.getResultByName("scrolling_time"), + scrollingDistance: r.getResultByName("scrolling_distance"), + })); + } + ); + info( + `Found ${interactions.length} interactions:\n ${JSON.stringify( + interactions + )}` + ); + Assert.equal( + interactions.length, + expected.length, + "Found the expected number of entries" + ); + for (let i = 0; i < Math.min(expected.length, interactions.length); i++) { + let actual = interactions[i]; + Assert.equal( + actual.url, + expected[i].url, + "Should have saved the page into the database" + ); + + if (expected[i].exactTotalViewTime != undefined) { + Assert.equal( + actual.totalViewTime, + expected[i].exactTotalViewTime, + "Should have kept the exact time." + ); + } else if (expected[i].totalViewTime != undefined) { + Assert.greaterOrEqual( + actual.totalViewTime, + expected[i].totalViewTime, + "Should have stored the interaction time" + ); + } + + if (expected[i].maxViewTime != undefined) { + Assert.less( + actual.totalViewTime, + expected[i].maxViewTime, + "Should have recorded an interaction below the maximum expected" + ); + } + + if (expected[i].keypresses != undefined) { + Assert.equal( + actual.keypresses, + expected[i].keypresses, + "Should have saved the keypresses into the database" + ); + } + + if (expected[i].exactTypingTime != undefined) { + Assert.equal( + actual.typingTime, + expected[i].exactTypingTime, + "Should have stored the exact typing time." + ); + } else if (expected[i].typingTimeIsGreaterThan != undefined) { + Assert.greater( + actual.typingTime, + expected[i].typingTimeIsGreaterThan, + "Should have stored at least this amount of typing time." + ); + } else if (expected[i].typingTimeIsLessThan != undefined) { + Assert.less( + actual.typingTime, + expected[i].typingTimeIsLessThan, + "Should have stored less than this amount of typing time." + ); + } + + if (expected[i].exactScrollingDistance != undefined) { + Assert.equal( + actual.scrollingDistance, + expected[i].exactScrollingDistance, + "Should have scrolled by exactly least this distance" + ); + } else if (expected[i].exactScrollingTime != undefined) { + Assert.greater( + actual.scrollingTime, + expected[i].exactScrollingTime, + "Should have scrolled for exactly least this duration" + ); + } + + if (expected[i].scrollingDistanceIsGreaterThan != undefined) { + Assert.greater( + actual.scrollingDistance, + expected[i].scrollingDistanceIsGreaterThan, + "Should have scrolled by at least this distance" + ); + } else if (expected[i].scrollingTimeIsGreaterThan != undefined) { + Assert.greater( + actual.scrollingTime, + expected[i].scrollingTimeIsGreaterThan, + "Should have scrolled for at least this duration" + ); + } + } +} + +/** + * Ensures that a list of interactions have been permanently stored. + * + * @param {string} url The url to query. + * @param {string} property The property to extract. + */ +async function getDatabaseValue(url, property) { + await Interactions.store.flush(); + const PROP_TRANSLATOR = { + totalViewTime: "total_view_time", + keypresses: "key_presses", + typingTime: "typing_time", + }; + property = PROP_TRANSLATOR[property] || property; + + return PlacesUtils.withConnectionWrapper( + "head.js::getDatabaseValue", + async db => { + let rows = await db.execute( + ` + SELECT * FROM moz_places_metadata m + JOIN moz_places h ON h.id = m.place_id + WHERE url = :url + ORDER BY created_at DESC + `, + { url } + ); + return rows?.[0].getResultByName(property); + } + ); +} diff --git a/browser/components/places/tests/browser/interactions/scrolling.html b/browser/components/places/tests/browser/interactions/scrolling.html new file mode 100644 index 0000000000..ce435097e6 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/scrolling.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> + +<html lang="en"> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=windows-1252"> + <style> + .div { + height: 250px; + width: 250px; + overflow: auto; + } + + .content { + height: 800px; + width: 2000px; + background-color: coral; + } + </style> +</head> +<body> + + <h1 id="heading" >Scrolling interaction tests</h1> + + <br><br><br> + + <div class="div" id="scroll_div" > + <div class="content" id="scrollable_div_content" tabindex="-1"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non. + Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br> + </div> + </div> + + <br> + + <br><br> + + <a href="#bottom" id="to_bottom_anchor">click to scroll to bottom</a> + <a id="top"></a> + + <br> + <iframe title="subframe" src="scrolling_subframe.html" id="subframe"> </iframe> + + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + + <div id="lowerDiv" class="div" > + <div class="content"> Scroll inside me!</div> + </div> + + <h1 id="middleHeading" > Middle </h1> + This is the middle anchor #1<a id="middleAnchor"></a> + + <a href="#top">click to scroll to top</a> + + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + This is the bottom anchor #3<a id="bottom"></a> + +</body> +</html> diff --git a/browser/components/places/tests/browser/interactions/scrolling_subframe.html b/browser/components/places/tests/browser/interactions/scrolling_subframe.html new file mode 100644 index 0000000000..55ad70f295 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/scrolling_subframe.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=windows-1252"> + <style> + .div { + height: 250px; + width: 250px; + overflow: auto; + } + .content { + height: 800px; + width: 2000px; + background-color: coral; + } + </style> +</head> +<body> + + Subframe Content<br> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non. + Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br> + +</body> +</html> diff --git a/browser/components/places/tests/browser/keyword_form.html b/browser/components/places/tests/browser/keyword_form.html new file mode 100644 index 0000000000..a881c0d5ad --- /dev/null +++ b/browser/components/places/tests/browser/keyword_form.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> + +<html lang="en"> +<head> + <meta http-equiv="Content-Type" content="text/html;charset=windows-1252"> +</head> +<body> + <form id="form1" method="POST" action="keyword_form.html"> + <input type="hidden" name="accenti" value="àèìòù"> + <input type="text" name="search"> + </form> + <form id="form2" method="POST" action="keyword_form.html"> + <input type="hidden" name="accenti" value="ùòìèà"> + <input type="text" name="search"> + </form> +</body> +</html> diff --git a/browser/components/places/tests/browser/pageopeningwindow.html b/browser/components/places/tests/browser/pageopeningwindow.html new file mode 100644 index 0000000000..855f67626e --- /dev/null +++ b/browser/components/places/tests/browser/pageopeningwindow.html @@ -0,0 +1,10 @@ +<meta charset="UTF-8"> +Hi, I was opened via a <script> +document.write(location.search ? + "popup call from the opened window... uh oh, that shouldn't happen!" : + "bookmarklet, and I will open a new window myself.");</script><br> +<script> + if (!location.search) { + open(location.href + "?donotopen=true", "_blank"); + } +</script> diff --git a/browser/components/places/tests/browser/sidebarpanels_click_test_page.html b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html new file mode 100644 index 0000000000..c73eaa5403 --- /dev/null +++ b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html @@ -0,0 +1,7 @@ +<html> +<head> + <title>browser_sidebarpanels_click.js test page</title> +</head> +<body onload="alert('test');"> +</body> +</html> diff --git a/browser/components/places/tests/chrome/chrome.toml b/browser/components/places/tests/chrome/chrome.toml new file mode 100644 index 0000000000..ca953fe898 --- /dev/null +++ b/browser/components/places/tests/chrome/chrome.toml @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = ["head.js"] + +["test_0_bug510634.xhtml"] + +["test_bug549192.xhtml"] + +["test_bug549491.xhtml"] + +["test_bug1163447_selectItems_through_shortcut.xhtml"] + +["test_selectItems_on_nested_tree.xhtml"] + +["test_treeview_date.xhtml"] diff --git a/browser/components/places/tests/chrome/head.js b/browser/components/places/tests/chrome/head.js new file mode 100644 index 0000000000..6a19fd89d3 --- /dev/null +++ b/browser/components/places/tests/chrome/head.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Services.scriptloader.loadSubScript( + "chrome://global/content/globalOverlay.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://browser/content/utilityOverlay.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +ChromeUtils.defineESModuleGetters(window, { + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs", +}); + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +XPCOMUtils.defineLazyScriptGetter( + window, + ["PlacesTreeView"], + "chrome://browser/content/places/treeView.js" +); +XPCOMUtils.defineLazyScriptGetter( + window, + ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"], + "chrome://browser/content/places/controller.js" +); diff --git a/browser/components/places/tests/chrome/test_0_bug510634.xhtml b/browser/components/places/tests/chrome/test_0_bug510634.xhtml new file mode 100644 index 0000000000..8cac56ff7c --- /dev/null +++ b/browser/components/places/tests/chrome/test_0_bug510634.xhtml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="510634: Wrong icons on bookmarks sidebar" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"> + <![CDATA[ + + /** + * Bug 510634 - Wrong icons on bookmarks sidebar + * https://bugzilla.mozilla.org/show_bug.cgi?id=510634 + * + * Ensures that properties for special queries are set on their tree nodes. + */ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + // Setup the places tree contents. + let tree = document.getElementById("tree"); + tree.place = `place:type=${Ci.nsINavHistoryQueryOptions.RESULTS_AS_LEFT_PANE_QUERY}&excludeItems=1&expandQueries=0`; + + // The query-property is set on the title column for each row. + let titleColumn = tree.columns.getColumnAt(0); + + // Open All Bookmarks + tree.selectItems([PlacesUtils.virtualAllBookmarksGuid]); + PlacesUtils.asContainer(tree.selectedNode).containerOpen = true; + is(tree.selectedNode.uri, + "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY, + "Opened All Bookmarks"); + + const topLevelGuids = [ + PlacesUtils.virtualHistoryGuid, + PlacesUtils.virtualDownloadsGuid, + PlacesUtils.virtualTagsGuid, + PlacesUtils.virtualAllBookmarksGuid + ]; + + for (let queryName of topLevelGuids) { + let found = false; + for (let i = 0; i < tree.view.rowCount && !found; i++) { + let rowProperties = tree.view.getCellProperties(i, titleColumn).split(" "); + found = rowProperties.includes("OrganizerQuery_" + queryName); + } + ok(found, `OrganizerQuery_${queryName} is set`); + } + + const folderGuids = [ + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.unfiledGuid, + ]; + + for (let guid of folderGuids) { + let found = false; + for (let i = 0; i < tree.view.rowCount && !found; i++) { + let rowProperties = tree.view.getCellProperties(i, titleColumn).split(" "); + found = rowProperties.includes("queryFolder_" + guid); + } + ok(found, `queryFolder_${guid} is set`); + } + + // Close the root node + tree.result.root.containerOpen = false; + + SimpleTest.finish(); + } + + ]]> + </script> +</window> diff --git a/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml new file mode 100644 index 0000000000..03f5d92572 --- /dev/null +++ b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml @@ -0,0 +1,88 @@ +<?xml version="1.0"?> + +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ + --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="1163447: selectItems in Places no longer selects items within Toolbar or Sidebar folders" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"><![CDATA[ + + /** + * Bug 1163447: places-tree should be able to select an item within the toolbar, and + * unfiled bookmarks. Yet not follow recursive folder-shortcuts infinitely. + */ + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + let bmu = PlacesUtils.bookmarks; + + await bmu.insert({ + parentGuid: bmu.toolbarGuid, + index: bmu.DEFAULT_INDEX, + type: bmu.TYPE_BOOKMARK, + url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`, + title: "shortcut to self - causing infinite recursion if not handled properly" + }); + + await bmu.insert({ + parentGuid: bmu.toolbarGuid, + index: bmu.DEFAULT_INDEX, + type: bmu.TYPE_BOOKMARK, + url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`, + title: "shortcut to unfiled, within toolbar" + }); + + let folder = await bmu.insert({ + parentGuid: bmu.unfiledGuid, + index: bmu.DEFAULT_INDEX, + type: bmu.TYPE_FOLDER, + title: "folder within unfiled" + }); + + // Setup the places tree contents. + let tree = document.getElementById("tree"); + tree.place = `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`; + + // Select the folder via the selectItems(folder.guid) API being tested + tree.selectItems([folder.guid]); + + is(tree.selectedNode && tree.selectedNode.bookmarkGuid, folder.guid, "The node was selected through the shortcut"); + + // Cleanup + await bmu.eraseEverything(); + + })().catch(err => { + ok(false, `Uncaught error: ${err}`); + }).then(SimpleTest.finish); + } + ]]></script> +</window> diff --git a/browser/components/places/tests/chrome/test_bug549192.xhtml b/browser/components/places/tests/chrome/test_bug549192.xhtml new file mode 100644 index 0000000000..9f00e8b9c5 --- /dev/null +++ b/browser/components/places/tests/chrome/test_bug549192.xhtml @@ -0,0 +1,130 @@ +<?xml version="1.0"?> + +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ + --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="549192: History view not updated after deleting entry" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flatList="true" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"><![CDATA[ + /** + * Bug 874407 + * Ensures that history views are updated properly after visits. + * Bug 549192 + * Ensures that history views are updated after deleting entries. + */ + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + await PlacesUtils.history.clear(); + + // Add some visits. + let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000); + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; + } + + const transition = PlacesUtils.history.TRANSITIONS.TYPED; + let places = + [{ uri: Services.io.newURI("http://example.tld/"), + visitDate: newTimeInMicroseconds(), transition }, + { uri: Services.io.newURI("http://example2.tld/"), + visitDate: newTimeInMicroseconds(), transition }, + { uri: Services.io.newURI("http://example3.tld/"), + visitDate: newTimeInMicroseconds(), transition }]; + + await PlacesTestUtils.addVisits(places); + + // Make a history query. + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let queryURI = PlacesUtils.history.queryToQueryString(query, opts); + + // Setup the places tree contents. + var tree = document.getElementById("tree"); + tree.place = queryURI; + + // loop through the rows and check them. + let treeView = tree.view; + let selection = treeView.selection; + let rc = treeView.rowCount; + + for (let i = 0; i < rc; i++) { + selection.select(i); + let node = tree.selectedNode; + is(node.uri, places[rc - i - 1].uri.spec, + "Found expected node at position " + i + "."); + } + + is(rc, 3, "Found expected number of rows."); + + // First check live-update of the view when adding visits. + places.forEach(place => place.visitDate = newTimeInMicroseconds()); + await PlacesTestUtils.addVisits(places); + + for (let i = 0; i < rc; i++) { + selection.select(i); + let node = tree.selectedNode; + is(node.uri, places[rc - i - 1].uri.spec, + "Found expected node at position " + i + "."); + } + + // Now remove the pages and verify live-update again. + for (let i = 0; i < rc; i++) { + selection.select(0); + let node = tree.selectedNode; + + const promiseRemoved = PlacesTestUtils.waitForNotification( + "page-removed", + events => events[0].url === node.uri + ); + + tree.controller.remove(); + + const removeEvents = await promiseRemoved; + ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + ok(treeView.treeIndexForNode(node) == -1, node.uri + " removed."); + is(treeView.rowCount, rc - i - 1, "Rows count decreased"); + } + + // Cleanup. + await PlacesUtils.history.clear(); + })().then(() => SimpleTest.finish()); + } + ]]></script> +</window> diff --git a/browser/components/places/tests/chrome/test_bug549491.xhtml b/browser/components/places/tests/chrome/test_bug549491.xhtml new file mode 100644 index 0000000000..03fee4cc06 --- /dev/null +++ b/browser/components/places/tests/chrome/test_bug549491.xhtml @@ -0,0 +1,78 @@ +<?xml version="1.0"?> + +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ + --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="549491: 'The root node is never visible' exception when details of the root node are modified " + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flatList="true" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Date" anonid="date" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"><![CDATA[ + /** + * Bug 549491 + * https://bugzilla.mozilla.org/show_bug.cgi?id=549491 + * + * Ensures that changing the details of places tree's root-node doesn't + * throw. + */ + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + await PlacesUtils.history.clear(); + + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://example.tld/"), + transition: PlacesUtils.history.TRANSITION_TYPED + }); + + // Make a history query. + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + let queryURI = PlacesUtils.history.queryToQueryString(query, opts); + + // Setup the places tree contents. + let tree = document.getElementById("tree"); + tree.place = queryURI; + + let rootNode = tree.result.root; + let obs = tree.view.QueryInterface(Ci.nsINavHistoryResultObserver); + obs.nodeHistoryDetailsChanged(rootNode, rootNode.time, rootNode.accessCount); + obs.nodeTitleChanged(rootNode, rootNode.title); + ok(true, "No exceptions thrown"); + + // Cleanup. + await PlacesUtils.history.clear(); + })().then(SimpleTest.finish); + } + ]]></script> +</window> diff --git a/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml new file mode 100644 index 0000000000..6dc2d33041 --- /dev/null +++ b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> + +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ + --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="549192: History view not updated after deleting entry" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"><![CDATA[ + /** + * Ensure that selectItems doesn't recurse infinitely in nested trees. + */ + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + (async function() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`, + title: "shortcut" + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}&maxResults=10`, + title: "query" + }); + + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "folder" + }); + + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.mozilla.org/", + title: "bookmark" + }); + + // Setup the places tree contents. + let tree = document.getElementById("tree"); + tree.place = `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`; + + // Select the last bookmark. + tree.selectItems([bm.guid]); + is (tree.selectedNode.bookmarkGuid, bm.guid, "The right node was selected"); + })().then(SimpleTest.finish); + } + ]]></script> +</window> diff --git a/browser/components/places/tests/chrome/test_treeview_date.xhtml b/browser/components/places/tests/chrome/test_treeview_date.xhtml new file mode 100644 index 0000000000..8a7853194d --- /dev/null +++ b/browser/components/places/tests/chrome/test_treeview_date.xhtml @@ -0,0 +1,159 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="435322: Places tree view's formatting" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://browser/content/places/places-tree.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" src="head.js" /> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + + <tree id="tree" + is="places-tree" + flatList="true" + flex="1"> + <treecols> + <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Tags" id="tags" anonid="tags" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Url" id="url" anonid="url" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Visit Date" id="date" anonid="date" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Visit Count" id="visitCount" anonid="visitCount" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <script type="application/javascript"> + <![CDATA[ + + /** + * Bug 435322 + * https://bugzilla.mozilla.org/show_bug.cgi?id=435322 + * + * Ensures that date in places treeviews is correctly formatted. + */ + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function uri(spec) { + return Services.io.newURI(spec); + } + + (async function() { + await PlacesUtils.history.clear(); + + let midnight = new Date(); + midnight.setHours(0); + midnight.setMinutes(0); + midnight.setSeconds(0); + midnight.setMilliseconds(0); + + // Add a visit 1ms before midnight, a visit at midnight, and + // a visit 1ms after midnight. + await PlacesTestUtils.addVisits([ + {uri: uri("http://before.midnight.com/"), + visitDate: (midnight.getTime() - 1) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED}, + {uri: uri("http://at.midnight.com/"), + visitDate: (midnight.getTime()) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED}, + {uri: uri("http://after.midnight.com/"), + visitDate: (midnight.getTime() + 1) * 1000, + transition: PlacesUtils.history.TRANSITION_TYPED} + ]); + + // add a bookmark to the midnight visit + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + url: "http://at.midnight.com/", + title: "A bookmark at midnight", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK + }); + + // Make a history query. + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + let queryURI = PlacesUtils.history.queryToQueryString(query, opts); + + // Setup the places tree contents. + let tree = document.getElementById("tree"); + tree.place = queryURI; + + // loop through the rows and check formatting + let treeView = tree.view; + let rc = treeView.rowCount; + ok(rc >= 3, "Rows found"); + let columns = tree.columns; + ok(columns.count > 0, "Columns found"); + for (let r = 0; r < rc; r++) { + let node = treeView.nodeForTreeIndex(r); + ok(node, "Places node found"); + for (let ci = 0; ci < columns.count; ci++) { + let c = columns.getColumnAt(ci); + let text = treeView.getCellText(r, c); + switch (c.element.getAttribute("anonid")) { + case "title": + // The title can differ, we did not set any title so we would + // expect null, but in such a case the view will generate a title + // through PlacesUIUtils.getBestTitle. + if (node.title) + is(text, node.title, "Title is correct"); + break; + case "url": + is(text, node.uri, "Uri is correct"); + break; + case "date": + let timeObj = new Date(node.time / 1000); + // Default is short date format. + let dtOptions = { + dateStyle: "short", + timeStyle: "short" + }; + + // For today's visits we don't show date portion. + if (node.uri == "http://at.midnight.com/" || + node.uri == "http://after.midnight.com/") { + dtOptions.dateStyle = undefined; + } else if (node.uri != "http://before.midnight.com/") { + // Avoid to test spurious uris, due to how the test works + // a redirecting uri could be put in the tree while we test. + break; + } + let timeStr = new Services.intl.DateTimeFormat(undefined, dtOptions).format(timeObj); + + is(text, timeStr, "Date format is correct"); + break; + case "visitCount": + is(text, 1, "Visit count is correct"); + break; + } + } + } + + // Cleanup. + await PlacesUtils.bookmarks.remove(bm.guid); + await PlacesUtils.history.clear(); + })().then(SimpleTest.finish); + } + ]]> + </script> +</window> diff --git a/browser/components/places/tests/marionette/manifest.toml b/browser/components/places/tests/marionette/manifest.toml new file mode 100644 index 0000000000..648f933814 --- /dev/null +++ b/browser/components/places/tests/marionette/manifest.toml @@ -0,0 +1,4 @@ +[DEFAULT] +run-if = ["buildapp == 'browser'"] + +["test_reopen_from_library.py"] diff --git a/browser/components/places/tests/marionette/test_reopen_from_library.py b/browser/components/places/tests/marionette/test_reopen_from_library.py new file mode 100644 index 0000000000..28f6ff3cd6 --- /dev/null +++ b/browser/components/places/tests/marionette/test_reopen_from_library.py @@ -0,0 +1,164 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import textwrap + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestReopenFromLibrary(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestReopenFromLibrary, self).setUp() + + self.original_showForNewBookmarks_pref = self.marionette.get_pref( + "browser.bookmarks.editDialog.showForNewBookmarks" + ) + self.original_loadBookmarksInTabs_pref = self.marionette.get_pref( + "browser.tabs.loadBookmarksInTabs" + ) + + self.marionette.set_pref( + "browser.bookmarks.editDialog.showForNewBookmarks", False + ) + self.marionette.set_pref("browser.tabs.loadBookmarksInTabs", True) + + def tearDown(self): + self.close_all_windows() + + self.marionette.restart(in_app=False, clean=True) + + super(TestReopenFromLibrary, self).tearDown() + + def test_open_bookmark_from_library_with_no_browser_window_open(self): + bookmark_url = self.marionette.absolute_url("empty.html") + self.marionette.navigate(bookmark_url) + + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + star_button = self.marionette.find_element("id", "star-button-box") + + # Await for async updates to the star button before clicking on it, as + # clicks are ignored during status updates. + script = """\ + return window.BookmarkingUI.status != window.BookmarkingUI.STATUS_UPDATING; + """ + Wait(self.marionette).until( + lambda _: self.marionette.execute_script(textwrap.dedent(script)), + message="Failed waiting for star updates", + ) + + star_button.click() + + star_image = self.marionette.find_element("id", "star-button") + + def check(_): + return "true" in star_image.get_attribute("starred") + + Wait(self.marionette).until(check, message="Failed to star the page") + + win = self.open_chrome_window( + "chrome://browser/content/places/places.xhtml", False + ) + + self.marionette.close_chrome_window() + + self.marionette.switch_to_window(win) + + # Tree elements can't be accessed in the same way as regular elements, + # so this uses some code from the places tests and EventUtils.js to + # select the bookmark in the tree and then double-click it. + script = """\ + window.PlacesOrganizer.selectLeftPaneContainerByHierarchy( + PlacesUtils.bookmarks.virtualToolbarGuid + ); + + // Bookmarks may be imported and shift the expected one, so search + // for it. + let node; + for (let i = 1; i < window.ContentTree.view.result.root.childCount; ++i) { + node = window.ContentTree.view.view.nodeForTreeIndex(i); + if (node.uri.endsWith("empty.html")) { + break; + } + } + + window.ContentTree.view.selectNode(node); + + // Based on synthesizeDblClickOnSelectedTreeCell + let tree = window.ContentTree.view; + + if (tree.view.selection.count < 1) { + throw new Error("The test node should be successfully selected"); + } + // Get selection rowID. + let min = {}; + let max = {}; + tree.view.selection.getRangeAt(0, min, max); + let rowID = min.value; + tree.ensureRowIsVisible(rowID); + // Calculate the click coordinates. + let rect = tree.getCoordsForCellItem(rowID, tree.columns[0], "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let treeBodyRect = tree.body.getBoundingClientRect(); + return [treeBodyRect.left + x, treeBodyRect.top + y] + """ + + position = self.marionette.execute_script(textwrap.dedent(script)) + # These must be integers for pointer_move + x = round(position[0]) + y = round(position[1]) + + self.marionette.actions.sequence( + "pointer", "pointer_id", {"pointerType": "mouse"} + ).pointer_move(x, y).click().click().perform() + + def window_with_url_open(_): + urls_in_windows = self.get_urls_for_windows() + + for urls in urls_in_windows: + if bookmark_url in urls: + return True + return False + + Wait(self.marionette).until( + window_with_url_open, + message="Failed to open the browser window from the library", + ) + + # Closes the library window. + self.marionette.close_chrome_window() + + def get_urls_for_windows(self): + # There's no guarantee that Marionette will return us an + # iterator for the opened windows that will match the + # order within our window list. Instead, we'll convert + # the list of URLs within each open window to a set of + # tuples that will allow us to do a direct comparison + # while allowing the windows to be in any order. + opened_urls = set() + for win in self.marionette.chrome_window_handles: + urls = tuple(self.get_urls_for_window(win)) + opened_urls.add(urls) + + return opened_urls + + def get_urls_for_window(self, win): + orig_handle = self.marionette.current_chrome_window_handle + + try: + self.marionette.switch_to_window(win) + return self.marionette.execute_script( + """ + if (!window?.gBrowser) { + return []; + } + return window.gBrowser.tabs.map(tab => { + return tab.linkedBrowser.currentURI.spec; + }); + """ + ) + finally: + self.marionette.switch_to_window(orig_handle) diff --git a/browser/components/places/tests/unit/bookmarks.glue.html b/browser/components/places/tests/unit/bookmarks.glue.html new file mode 100644 index 0000000000..07b22e9b3f --- /dev/null +++ b/browser/components/places/tests/unit/bookmarks.glue.html @@ -0,0 +1,16 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1>Bookmarks Menu</H1> + +<DL><p> + <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A> + <DT><H3 ADD_DATE="1233157910" LAST_MODIFIED="1233157972" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3> +<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar + <DL><p> + <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A> + </DL><p> +</DL><p> diff --git a/browser/components/places/tests/unit/bookmarks.glue.json b/browser/components/places/tests/unit/bookmarks.glue.json new file mode 100644 index 0000000000..069f605d29 --- /dev/null +++ b/browser/components/places/tests/unit/bookmarks.glue.json @@ -0,0 +1,83 @@ +{ + "title": "", + "id": 1, + "dateAdded": 1233157910552624, + "lastModified": 1233157955206833, + "type": "text/x-moz-place-container", + "root": "placesRoot", + "children": [ + { + "title": "Bookmarks Menu", + "id": 2, + "parent": 1, + "dateAdded": 1233157910552624, + "lastModified": 1233157993171424, + "type": "text/x-moz-place-container", + "root": "bookmarksMenuFolder", + "children": [ + { + "title": "examplejson", + "id": 27, + "parent": 2, + "dateAdded": 1233157972101126, + "lastModified": 1233157984999673, + "type": "text/x-moz-place", + "uri": "http://example.com/" + } + ] + }, + { + "index": 1, + "title": "Bookmarks Toolbar", + "id": 3, + "parent": 1, + "dateAdded": 1233157910552624, + "lastModified": 1233157972101126, + "annos": [ + { + "name": "bookmarkProperties/description", + "flags": 0, + "expires": 4, + "mimeType": null, + "type": 3, + "value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar" + } + ], + "type": "text/x-moz-place-container", + "root": "toolbarFolder", + "children": [ + { + "title": "examplejson", + "id": 26, + "parent": 3, + "dateAdded": 1233157972101126, + "lastModified": 1233157984999673, + "type": "text/x-moz-place", + "uri": "http://example.com/" + } + ] + }, + { + "index": 2, + "title": "Tags", + "id": 4, + "parent": 1, + "dateAdded": 1233157910552624, + "lastModified": 1233157910582667, + "type": "text/x-moz-place-container", + "root": "tagsFolder", + "children": [] + }, + { + "index": 3, + "title": "Other Bookmarks", + "id": 5, + "parent": 1, + "dateAdded": 1233157910552624, + "lastModified": 1233157911033315, + "type": "text/x-moz-place-container", + "root": "unfiledBookmarksFolder", + "children": [] + } + ] +} diff --git a/browser/components/places/tests/unit/corruptDB.sqlite b/browser/components/places/tests/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 0000000000..b234246cac --- /dev/null +++ b/browser/components/places/tests/unit/corruptDB.sqlite diff --git a/browser/components/places/tests/unit/distribution.ini b/browser/components/places/tests/unit/distribution.ini new file mode 100644 index 0000000000..a25c40fed3 --- /dev/null +++ b/browser/components/places/tests/unit/distribution.ini @@ -0,0 +1,30 @@ +# Distribution Configuration File +# Bug 516444 demo + +[Global] +id=516444 +version=1.0 +about=Test distribution file + +[BookmarksToolbar] +item.1.title=Toolbar Link Before +item.1.link=https://example.org/toolbar/before/ +item.1.icon=https://example.org/favicon.png +item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg== +item.2.type=default +item.3.type=folder +item.3.title=Toolbar Folder After +item.3.folderId=1 + +[BookmarksMenu] +item.1.title=Menu Link Before +item.1.link=https://example.org/menu/before/ +item.1.icon=https://example.org/favicon.png +item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg== +item.2.type=default +item.3.title=Menu Link After +item.3.link=https://example.org/menu/after/ + +[BookmarksFolder-1] +item.1.title=Toolbar Link Folder +item.1.link=https://example.org/toolbar/folder/ diff --git a/browser/components/places/tests/unit/head_bookmarks.js b/browser/components/places/tests/unit/head_bookmarks.js new file mode 100644 index 0000000000..db6bfe8f0f --- /dev/null +++ b/browser/components/places/tests/unit/head_bookmarks.js @@ -0,0 +1,82 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Import common head. +/* import-globals-from ../../../../../toolkit/components/places/tests/head_common.js */ +var commonFile = do_get_file( + "../../../../../toolkit/components/places/tests/head_common.js", + false +); +if (commonFile) { + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + +ChromeUtils.defineESModuleGetters(this, { + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", +}); + +// Needed by some test that relies on having an app registered. +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "PlacesTest", + ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", + version: "1", + platformVersion: "", +}); + +// Default bookmarks constants. +const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1; +const DEFAULT_BOOKMARKS_ON_MENU = 1; + +var createCorruptDB = async function () { + let dbPath = PathUtils.join(PathUtils.profileDir, "places.sqlite"); + await IOUtils.remove(dbPath); + + // Create a corrupt database. + let src = PathUtils.join(do_get_cwd().path, "corruptDB.sqlite"); + await IOUtils.copy(src, dbPath); + + // Check there's a DB now. + Assert.ok(await IOUtils.exists(dbPath), "should have a DB now"); +}; + +const SINGLE_TRY_TIMEOUT = 100; +const NUMBER_OF_TRIES = 30; + +/** + * Similar to waitForConditionPromise, but poll for an asynchronous value + * every SINGLE_TRY_TIMEOUT ms, for no more than tryCount times. + * + * @param {Function} promiseFn + * A function to generate a promise, which resolves to the expected + * asynchronous value. + * @param {msg} timeoutMsg + * The reason to reject the returned promise with. + * @param {number} [tryCount] + * Maximum times to try before rejecting the returned promise with + * timeoutMsg, defaults to NUMBER_OF_TRIES. + * @returns {Promise} to the asynchronous value being polled. + * @throws if the asynchronous value is not available after tryCount attempts. + */ +var waitForResolvedPromise = async function ( + promiseFn, + timeoutMsg, + tryCount = NUMBER_OF_TRIES +) { + let tries = 0; + do { + try { + let value = await promiseFn(); + return value; + } catch (ex) {} + await new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve)); + } while (++tries <= tryCount); + throw new Error(timeoutMsg); +}; diff --git a/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js b/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js new file mode 100644 index 0000000000..58b68d7574 --- /dev/null +++ b/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js @@ -0,0 +1,107 @@ +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +add_task(async function test_no_result_node() { + let functionSpy = sinon.stub().returns(Promise.resolve()); + + await PlacesUIUtils.batchUpdatesForNode(null, 1, functionSpy); + + Assert.ok( + functionSpy.calledOnce, + "Passing a null result node should still call the wrapped function" + ); +}); + +add_task(async function test_under_batch_threshold() { + let functionSpy = sinon.stub().returns(Promise.resolve()); + let resultNode = { + QueryInterface() { + return this; + }, + onBeginUpdateBatch: sinon.spy(), + onEndUpdateBatch: sinon.spy(), + }; + + await PlacesUIUtils.batchUpdatesForNode(resultNode, 1, functionSpy); + + Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once"); + Assert.ok( + resultNode.onBeginUpdateBatch.notCalled, + "onBeginUpdateBatch should not have been called" + ); + Assert.ok( + resultNode.onEndUpdateBatch.notCalled, + "onEndUpdateBatch should not have been called" + ); +}); + +add_task(async function test_over_batch_threshold() { + let functionSpy = sinon.stub().callsFake(() => { + Assert.ok( + resultNode.onBeginUpdateBatch.calledOnce, + "onBeginUpdateBatch should have been called before the function" + ); + Assert.ok( + resultNode.onEndUpdateBatch.notCalled, + "onEndUpdateBatch should not have been called before the function" + ); + + return Promise.resolve(); + }); + let resultNode = { + QueryInterface() { + return this; + }, + onBeginUpdateBatch: sinon.spy(), + onEndUpdateBatch: sinon.spy(), + }; + + await PlacesUIUtils.batchUpdatesForNode(resultNode, 100, functionSpy); + + Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once"); + Assert.ok( + resultNode.onBeginUpdateBatch.calledOnce, + "onBeginUpdateBatch should have been called" + ); + Assert.ok( + resultNode.onEndUpdateBatch.calledOnce, + "onEndUpdateBatch should have been called" + ); +}); + +add_task(async function test_wrapped_function_throws() { + let error = new Error("Failed!"); + let functionSpy = sinon.stub().throws(error); + let resultNode = { + QueryInterface() { + return this; + }, + onBeginUpdateBatch: sinon.spy(), + onEndUpdateBatch: sinon.spy(), + }; + + let raisedError; + try { + await PlacesUIUtils.batchUpdatesForNode(resultNode, 100, functionSpy); + } catch (ex) { + raisedError = ex; + } + + Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once"); + Assert.ok( + resultNode.onBeginUpdateBatch.calledOnce, + "onBeginUpdateBatch should have been called" + ); + Assert.ok( + resultNode.onEndUpdateBatch.calledOnce, + "onEndUpdateBatch should have been called" + ); + Assert.equal( + raisedError, + error, + "batchUpdatesForNode should have raised the error from the wrapped function" + ); +}); diff --git a/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js b/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js new file mode 100644 index 0000000000..db213971e9 --- /dev/null +++ b/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js @@ -0,0 +1,141 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +const UTF8 = "UTF-8"; +const UTF16 = "UTF-16"; + +const TEST_URI = "http://foo.com"; +const TEST_BOOKMARKED_URI = "http://bar.com"; + +add_task(function setup() { + let savedIsWindowPrivateFunc = PrivateBrowsingUtils.isWindowPrivate; + PrivateBrowsingUtils.isWindowPrivate = () => false; + + registerCleanupFunction(() => { + PrivateBrowsingUtils.isWindowPrivate = savedIsWindowPrivateFunc; + }); +}); + +add_task(async function test_simple_add() { + // add pages to history + await PlacesTestUtils.addVisits(TEST_URI); + await PlacesTestUtils.addVisits(TEST_BOOKMARKED_URI); + + // create bookmarks on TEST_BOOKMARKED_URI + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_BOOKMARKED_URI, + title: TEST_BOOKMARKED_URI.spec, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_BOOKMARKED_URI, + title: TEST_BOOKMARKED_URI.spec, + }); + + // set charset on not-bookmarked page + await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {}); + // set charset on bookmarked page + await PlacesUIUtils.setCharsetForPage(TEST_BOOKMARKED_URI, UTF16, {}); + + let pageInfo = await PlacesUtils.history.fetch(TEST_URI, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + UTF16, + "Should return correct charset for a not-bookmarked page" + ); + + pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + UTF16, + "Should return correct charset for a bookmarked page" + ); + + await PlacesUtils.history.clear(); + + pageInfo = await PlacesUtils.history.fetch(TEST_URI, { + includeAnnotations: true, + }); + Assert.ok( + !pageInfo, + "Should not return pageInfo for a page after history cleared" + ); + + pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + UTF16, + "Charset should still be set for a bookmarked page after history clear" + ); + + await PlacesUIUtils.setCharsetForPage(TEST_BOOKMARKED_URI, ""); + pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, { + includeAnnotations: true, + }); + Assert.strictEqual( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + undefined, + "Should not have a charset after it has been removed from the page" + ); +}); + +add_task(async function test_utf8_clears_saved_anno() { + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(TEST_URI); + + // set charset on bookmarked page + await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {}); + + let pageInfo = await PlacesUtils.history.fetch(TEST_URI, { + includeAnnotations: true, + }); + Assert.equal( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + UTF16, + "Should return correct charset for a not-bookmarked page" + ); + + // Now set the bookmark to a UTF-8 charset. + await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF8, {}); + + pageInfo = await PlacesUtils.history.fetch(TEST_URI, { + includeAnnotations: true, + }); + Assert.strictEqual( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + undefined, + "Should have removed the charset for a UTF-8 page." + ); +}); + +add_task(async function test_private_browsing_not_saved() { + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(TEST_URI); + + // set charset on bookmarked page, but pretend this is a private browsing window. + PrivateBrowsingUtils.isWindowPrivate = () => true; + await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {}); + + let pageInfo = await PlacesUtils.history.fetch(TEST_URI, { + includeAnnotations: true, + }); + Assert.strictEqual( + pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO), + undefined, + "Should not have set the charset in a private browsing window." + ); +}); diff --git a/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js b/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js new file mode 100644 index 0000000000..aaa4db6bb2 --- /dev/null +++ b/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the difference indication for titles. + */ + +const TESTS = [ + { + title: null, + expected: undefined, + }, + { + title: "Short title", + expected: undefined, + }, + { + title: "Short title2", + expected: undefined, + }, + { + title: "Long title same as another", + expected: undefined, + }, + { + title: "Long title same as another", + expected: undefined, + }, + { + title: "Long title with difference at the end 1", + expected: 38, + }, + { + title: "Long title with difference at the end 2", + expected: 38, + }, + { + title: "A long title with difference 123456 in the middle.", + expected: 30, + }, + { + title: "A long title with difference 135246 in the middle.", + expected: 30, + }, + { + title: + "Some long titles with variable 12345678 differences to 13572468 other titles", + expected: 32, + }, + { + title: + "Some long titles with variable 12345678 differences to 15263748 other titles", + expected: 32, + }, + { + title: + "Some long titles with variable 15263748 differences to 12345678 other titles", + expected: 32, + }, + { + title: "One long title which will be shorter than the other one", + expected: 40, + }, + { + title: + "One long title which will be shorter that the other one (not this one)", + expected: 40, + }, +]; + +add_task(async function test_difference_finding() { + PlacesUIUtils.insertTitleStartDiffs(TESTS); + + for (let result of TESTS) { + Assert.equal( + result.titleDifferentIndex, + result.expected, + `Should have returned the correct index for "${result.title}"` + ); + } +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js new file mode 100644 index 0000000000..65a038487a --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js @@ -0,0 +1,33 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that nsBrowserGlue correctly exports bookmarks.html at shutdown if + * browser.bookmarks.autoExportHTML is set to true. + */ + +add_task(async function () { + remove_bookmarks_html(); + + Services.prefs.setBoolPref("browser.bookmarks.autoExportHTML", true); + registerCleanupFunction(() => + Services.prefs.clearUserPref("browser.bookmarks.autoExportHTML") + ); + + // Initialize nsBrowserGlue before Places. + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports); + + // Initialize Places through the History Service. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE + ); + + Services.obs.addObserver(function observer() { + Services.obs.removeObserver(observer, "profile-before-change"); + check_bookmarks_html(); + }, "profile-before-change"); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt.js b/browser/components/places/tests/unit/test_browserGlue_corrupt.js new file mode 100644 index 0000000000..0514b06bbc --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if + * database is corrupt and one backup is available. + */ + +function run_test() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); + + remove_all_JSON_backups(); + + // Create our JSON backup from bookmarks.glue.json. + create_JSON_backup("bookmarks.glue.json"); + + run_next_test(); +} + +registerCleanupFunction(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + return PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_main() { + // Create a corrupt database. + await createCorruptDB(); + + // Initialize nsBrowserGlue before Places. + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports); + + // Check the database was corrupt. + // nsBrowserGlue uses databaseStatus to manage initialization. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT + ); + + // The test will continue once restore has finished. + await promiseTopicObserved("places-browser-init-complete"); + + // Check that JSON backup has been restored. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + Assert.equal(bm.title, "examplejson"); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js new file mode 100644 index 0000000000..a0bc93c74c --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that nsBrowserGlue correctly imports from bookmarks.html if database + * is corrupt but a JSON backup is not available. + */ + +function run_test() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); + + // Remove JSON backup from profile. + remove_all_JSON_backups(); + + run_next_test(); +} + +registerCleanupFunction(remove_bookmarks_html); + +add_task(async function () { + // Create a corrupt database. + await createCorruptDB(); + + // Initialize nsBrowserGlue before Places. + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports); + + // Check the database was corrupt. + // nsBrowserGlue uses databaseStatus to manage initialization. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT + ); + + // The test will continue once import has finished. + await promiseTopicObserved("places-browser-init-complete"); + + // Check that bookmarks html has been restored. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + Assert.equal(bm.title, "example"); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js new file mode 100644 index 0000000000..031d82d9e2 --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that nsBrowserGlue correctly restores default bookmarks if database is + * corrupt, nor a JSON backup nor bookmarks.html are available. + */ + +function run_test() { + // Remove bookmarks.html from profile. + remove_bookmarks_html(); + + // Remove JSON backup from profile. + remove_all_JSON_backups(); + + run_next_test(); +} + +add_task(async function () { + // Create a corrupt database. + await createCorruptDB(); + + // Initialize nsBrowserGlue before Places. + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports); + + // Check the database was corrupt. + // nsBrowserGlue uses databaseStatus to manage initialization. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT + ); + + // The test will continue once import has finished. + await promiseTopicObserved("places-browser-init-complete"); + + // Check that default bookmarks have been restored. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + + // Bug 1283076: Nightly bookmark points to Get Involved page, not Getting Started one + let chanTitle = AppConstants.NIGHTLY_BUILD + ? "Get Involved" + : "Getting Started"; + Assert.equal(bm.title, chanTitle); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_distribution.js b/browser/components/places/tests/unit/test_browserGlue_distribution.js new file mode 100644 index 0000000000..5fafba457f --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that nsBrowserGlue correctly imports bookmarks from distribution.ini. + */ + +const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed"; +const PREF_DISTRIBUTION_ID = "distribution.id"; + +const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization"; +const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete"; +const TOPIC_BROWSERGLUE_TEST = "browser-glue-test"; + +function run_test() { + // Set special pref to load distribution.ini from the profile folder. + Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true); + + // Copy distribution.ini file to the profile dir. + let distroDir = gProfD.clone(); + distroDir.leafName = "distribution"; + let iniFile = distroDir.clone(); + iniFile.append("distribution.ini"); + if (iniFile.exists()) { + iniFile.remove(false); + print("distribution.ini already exists, did some test forget to cleanup?"); + } + + let testDistributionFile = gTestDir.clone(); + testDistributionFile.append("distribution.ini"); + testDistributionFile.copyTo(distroDir, "distribution.ini"); + Assert.ok(testDistributionFile.exists()); + + run_next_test(); +} + +registerCleanupFunction(function () { + // Remove the distribution file, even if the test failed, otherwise all + // next tests will import it. + let iniFile = gProfD.clone(); + iniFile.leafName = "distribution"; + iniFile.append("distribution.ini"); + if (iniFile.exists()) { + iniFile.remove(false); + } + Assert.ok(!iniFile.exists()); +}); + +add_task(async function () { + let { DistributionCustomizer } = ChromeUtils.importESModule( + "resource:///modules/distribution.sys.mjs" + ); + let distribution = new DistributionCustomizer(); + + let glue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver + ); + // Initialize Places through the History Service and check that a new + // database has been created. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE + ); + // Force distribution. + glue.observe( + null, + TOPIC_BROWSERGLUE_TEST, + TOPICDATA_DISTRIBUTION_CUSTOMIZATION + ); + + // Test will continue on customization complete notification. + await promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE); + + // Check the custom bookmarks exist on menu. + let menuItem = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0, + }); + Assert.equal(menuItem.title, "Menu Link Before"); + Assert.ok( + menuItem.guid.startsWith(distribution.BOOKMARK_GUID_PREFIX), + "Guid of this bookmark has expected prefix" + ); + + menuItem = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1 + DEFAULT_BOOKMARKS_ON_MENU, + }); + Assert.equal(menuItem.title, "Menu Link After"); + + // Check no favicon exists for this bookmark + await Assert.rejects( + waitForResolvedPromise( + () => { + return PlacesUtils.promiseFaviconData(menuItem.url.href); + }, + "Favicon not found", + 10 + ), + /Favicon\snot\sfound/, + "Favicon not found" + ); + + // Check the custom bookmarks exist on toolbar. + let toolbarItem = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + Assert.equal(toolbarItem.title, "Toolbar Link Before"); + + // Check the custom favicon exist for this bookmark + let faviconItem = await waitForResolvedPromise( + () => { + return PlacesUtils.promiseFaviconData(toolbarItem.url.href); + }, + "Favicon not found", + 10 + ); + Assert.equal(faviconItem.uri.spec, "https://example.org/favicon.png"); + Assert.greater(faviconItem.dataLen, 0); + Assert.equal(faviconItem.mimeType, "image/png"); + + let base64Icon = + "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, faviconItem.data)); + Assert.equal(base64Icon, SMALLPNG_DATA_URI.spec); + + toolbarItem = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR, + }); + Assert.equal(toolbarItem.title, "Toolbar Folder After"); + Assert.ok( + toolbarItem.guid.startsWith(distribution.FOLDER_GUID_PREFIX), + "Guid of this folder has expected prefix" + ); + + // Check the bmprocessed pref has been created. + Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED)); + + // Check distribution prefs have been created. + Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444"); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_migrate.js b/browser/components/places/tests/unit/test_browserGlue_migrate.js new file mode 100644 index 0000000000..f0f88d2d17 --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that nsBrowserGlue does not overwrite bookmarks imported from the + * migrators. They usually run before nsBrowserGlue, so if we find any + * bookmark on init, we should not try to import. + */ + +function run_test() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); + + // Remove current database file. + clearDB(); + + run_next_test(); +} + +registerCleanupFunction(remove_bookmarks_html); + +add_task(async function test_migrate_bookmarks() { + // Initialize Places through the History Service and check that a new + // database has been created. + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE + ); + + // A migrator would run before nsBrowserGlue Places initialization, so mimic + // that behavior adding a bookmark and notifying the migration. + let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver); + bg.observe(null, "initial-migration-will-import-default-bookmarks", null); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/", + title: "migrated", + }); + + let promise = promiseTopicObserved("places-browser-init-complete"); + bg.observe(null, "initial-migration-did-import-default-bookmarks", null); + await promise; + + // Check the created bookmark still exists. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0, + }); + Assert.equal(bm.title, "migrated"); + + // Check that we have not imported any new bookmark. + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1, + })) + ); + + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + })) + ); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js new file mode 100644 index 0000000000..af1dc3db0e --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that nsBrowserGlue is correctly interpreting the preferences settable + * by the user or by other components. + */ + +const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML"; +const PREF_RESTORE_DEFAULT_BOOKMARKS = + "browser.bookmarks.restore_default_bookmarks"; +const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML"; + +const TOPIC_BROWSERGLUE_TEST = "browser-glue-test"; +const TOPICDATA_FORCE_PLACES_INIT = "test-force-places-init"; + +var bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver); + +add_task(async function setup() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); + + remove_all_JSON_backups(); + + // Create our JSON backup from bookmarks.glue.json. + create_JSON_backup("bookmarks.glue.json"); + + registerCleanupFunction(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + + return PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +function simulatePlacesInit() { + info("Simulate Places init"); + // Force nsBrowserGlue::_initPlaces(). + bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT); + return promiseTopicObserved("places-browser-init-complete"); +} + +add_task(async function test_checkPreferences() { + // Initialize Places through the History Service and check that a new + // database has been created. + let promiseComplete = promiseTopicObserved("places-browser-init-complete"); + Assert.equal( + PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE + ); + await promiseComplete; + + // Ensure preferences status. + Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); + + Assert.throws( + () => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML), + /NS_ERROR_UNEXPECTED/ + ); + Assert.throws( + () => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS), + /NS_ERROR_UNEXPECTED/ + ); +}); + +add_task(async function test_import() { + info("Import from bookmarks.html if importBookmarksHTML is true."); + + await PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + })) + ); + + // Set preferences. + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + + await simulatePlacesInit(); + + // Check bookmarks.html has been imported. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + Assert.equal(bm.title, "example"); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); +}); + +add_task(async function test_restore() { + info( + "restore from default bookmarks.html if " + + "restore_default_bookmarks is true." + ); + + await PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + })) + ); + + // Set preferences. + Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); + + await simulatePlacesInit(); + + // Check bookmarks.html has been restored. + Assert.ok( + await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }) + ); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); +}); + +add_task(async function test_restore_import() { + info( + "setting both importBookmarksHTML and " + + "restore_default_bookmarks should restore defaults." + ); + + await PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok( + !(await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + })) + ); + + // Set preferences. + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); + + await simulatePlacesInit(); + + // Check bookmarks.html has been restored. + Assert.ok( + await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }) + ); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); +}); diff --git a/browser/components/places/tests/unit/test_browserGlue_restore.js b/browser/components/places/tests/unit/test_browserGlue_restore.js new file mode 100644 index 0000000000..98c8d1d2d6 --- /dev/null +++ b/browser/components/places/tests/unit/test_browserGlue_restore.js @@ -0,0 +1,55 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if + * database has been created and one backup is available. + */ + +function run_test() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); + + remove_all_JSON_backups(); + + // Create our JSON backup from bookmarks.glue.json. + create_JSON_backup("bookmarks.glue.json"); + + // Remove current database file. + clearDB(); + + run_next_test(); +} + +registerCleanupFunction(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + return PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_main() { + // Initialize nsBrowserGlue before Places. + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports); + + // Initialize Places through the History Service. + let hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); + + // Check a new database has been created. + // nsBrowserGlue uses databaseStatus to manage initialization. + Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE); + + // The test will continue once restore has finished. + await promiseTopicObserved("places-browser-init-complete"); + + // Check that JSON backup has been restored. + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + Assert.equal(bm.title, "examplejson"); +}); diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js new file mode 100644 index 0000000000..27b432e569 --- /dev/null +++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js @@ -0,0 +1,183 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that requesting clear history at shutdown will really clear history. + */ + +const URIS = [ + "http://a.example1.com/", + "http://b.example1.com/", + "http://b.example2.com/", + "http://c.example3.com/", +]; + +const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/"; + +const { Sanitizer } = ChromeUtils.importESModule( + "resource:///modules/Sanitizer.sys.mjs" +); + +// Send the profile-after-change notification to the form history component to ensure +// that it has been initialized. +var formHistoryStartup = Cc[ + "@mozilla.org/satchel/form-history-startup;1" +].getService(Ci.nsIObserver); +formHistoryStartup.observe(null, "profile-after-change", null); +ChromeUtils.defineESModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", +}); + +var timeInMicroseconds = Date.now() * 1000; + +add_task(async function test_execute() { + info("Initialize browserglue before Places"); + + // Avoid default bookmarks import. + let glue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver + ); + glue.observe(null, "initial-migration-will-import-default-bookmarks", null); + Sanitizer.onStartup(); + + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cache", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "offlineApps", + true + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "history", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "downloads", + true + ); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formData", true); + Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "sessions", true); + Services.prefs.setBoolPref( + Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings", + true + ); + + Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true); + + info("Add visits."); + for (let aUrl of URIS) { + await PlacesTestUtils.addVisits({ + uri: uri(aUrl), + visitDate: timeInMicroseconds++, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + info("Add cache."); + await storeCache(FTP_URL, "testData"); + info("Add form history."); + await addFormHistory(); + Assert.equal(await getFormHistoryCount(), 1, "Added form history"); + + info("Simulate and wait shutdown."); + await shutdownPlaces(); + + Assert.equal(await getFormHistoryCount(), 0, "Form history cleared"); + + let stmt = DBConn(true).createStatement( + "SELECT id FROM moz_places WHERE url = :page_url " + ); + + try { + URIS.forEach(function (aUrl) { + stmt.params.page_url = aUrl; + Assert.ok(!stmt.executeStep()); + stmt.reset(); + }); + } finally { + stmt.finalize(); + } + + info("Check cache"); + // Check cache. + await checkCache(FTP_URL); +}); + +function addFormHistory() { + let now = Date.now() * 1000; + return FormHistory.update({ + op: "add", + fieldname: "testfield", + value: "test", + timesUsed: 1, + firstUsed: now, + lastUsed: now, + }); +} + +async function getFormHistoryCount() { + return FormHistory.count({ fieldname: "testfield" }); +} + +function storeCache(aURL, aContent) { + let cache = Services.cache2; + let storage = cache.diskCacheStorage(Services.loadContextInfo.default); + + return new Promise(resolve => { + let storeCacheListener = { + onCacheEntryCheck(entry) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, status) { + Assert.equal(status, Cr.NS_OK); + + entry.setMetaDataElement("servertype", "0"); + var os = entry.openOutputStream(0, -1); + + var written = os.write(aContent, aContent.length); + if (written != aContent.length) { + do_throw( + "os.write has not written all data!\n" + + " Expected: " + + written + + "\n" + + " Actual: " + + aContent.length + + "\n" + ); + } + os.close(); + entry.close(); + resolve(); + }, + }; + + storage.asyncOpenURI( + Services.io.newURI(aURL), + "", + Ci.nsICacheStorage.OPEN_NORMALLY, + storeCacheListener + ); + }); +} + +function checkCache(aURL) { + let cache = Services.cache2; + let storage = cache.diskCacheStorage(Services.loadContextInfo.default); + + return new Promise(resolve => { + let checkCacheListener = { + onCacheEntryAvailable(entry, isnew, status) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + resolve(); + }, + }; + + storage.asyncOpenURI( + Services.io.newURI(aURL), + "", + Ci.nsICacheStorage.OPEN_READONLY, + checkCacheListener + ); + }); +} diff --git a/browser/components/places/tests/unit/test_interactions_blocklist.js b/browser/components/places/tests/unit/test_interactions_blocklist.js new file mode 100644 index 0000000000..0e81f80af2 --- /dev/null +++ b/browser/components/places/tests/unit/test_interactions_blocklist.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that blocked sites are caught by InteractionsBlocklist. + */ + +ChromeUtils.defineESModuleGetters(this, { + InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs", +}); + +let BLOCKED_URLS = [ + "https://www.bing.com/search?q=mozilla", + "https://duckduckgo.com/?q=a+test&kp=1&t=ffab", + "https://www.google.com/search?q=mozilla", + "https://www.google.ca/search?q=test", + "https://mozilla.zoom.us/j/123456789", + "https://yandex.az/search/?text=mozilla", + "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=mozilla&rn=&fenlei=256&oq=&rsv_pq=970f2b8f001757b9&rsv_t=1f5d2V2o80HPdZtZnhodwkc7nZXTvDI1zwdPy%2FAeomnvFFGIrU1F3D9WoK4&rqlang=cn", + "https://accounts.google.com/o/oauth2/v2/auth/identifier/foobar", + "https://auth.mozilla.auth0.com/login/foobar", + "https://accounts.google.com/signin/oauth/consent/foobar", + "https://accounts.google.com/o/oauth2/v2/auth?client_id=ZZZ", + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize/foobar", +]; + +let ALLOWED_URLS = [ + "https://example.com", + "https://zoom.us/pricing", + "https://www.google.ca/maps/place/Toronto,+ON/@43.7181557,-79.5181414,11z/data=!3m1!4b1!4m5!3m4!1s0x89d4cb90d7c63ba5:0x323555502ab4c477!8m2!3d43.653226!4d-79.3831843", + "https://example.com/https://auth.mozilla.auth0.com/login/foobar", +]; + +// Tests that initializing InteractionsBlocklist loads the regexes from the +// customBlocklist pref on initialization. This subtest should always be the +// first one in this file. +add_task(async function blockedOnInit() { + Services.prefs.setStringPref( + "places.interactions.customBlocklist", + '["^(https?:\\\\/\\\\/)?mochi.test"]' + ); + Assert.ok( + InteractionsBlocklist.isUrlBlocklisted("https://mochi.test"), + "mochi.test is blocklisted." + ); + InteractionsBlocklist.removeRegexFromBlocklist("^(https?:\\/\\/)?mochi.test"); + Assert.ok( + !InteractionsBlocklist.isUrlBlocklisted("https://mochi.test"), + "mochi.test is not blocklisted." + ); +}); + +add_task(async function test() { + for (let url of BLOCKED_URLS) { + Assert.ok( + InteractionsBlocklist.isUrlBlocklisted(url), + `${url} is blocklisted.` + ); + } + + for (let url of ALLOWED_URLS) { + Assert.ok( + !InteractionsBlocklist.isUrlBlocklisted(url), + `${url} is not blocklisted.` + ); + } +}); diff --git a/browser/components/places/tests/unit/test_invalid_defaultLocation.js b/browser/components/places/tests/unit/test_invalid_defaultLocation.js new file mode 100644 index 0000000000..e533d051d0 --- /dev/null +++ b/browser/components/places/tests/unit/test_invalid_defaultLocation.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test that if browser.bookmarks.defaultLocation contains an invalid GUID, + * PlacesUIUtils.defaultParentGuid will return a proper default value. + */ + +add_task(async function () { + Services.prefs.setCharPref( + "browser.bookmarks.defaultLocation", + "useOtherBookmarks" + ); + + info( + "Checking that default parent guid was set back to the toolbar because of invalid preferable guid" + ); + Assert.equal( + await PlacesUIUtils.defaultParentGuid, + PlacesUtils.bookmarks.toolbarGuid, + "Default parent guid is a toolbar guid" + ); +}); diff --git a/browser/components/places/tests/unit/xpcshell.toml b/browser/components/places/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..75727e359a --- /dev/null +++ b/browser/components/places/tests/unit/xpcshell.toml @@ -0,0 +1,38 @@ +[DEFAULT] +head = "head_bookmarks.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] # bug 1730213 +support-files = [ + "bookmarks.glue.html", + "bookmarks.glue.json", + "corruptDB.sqlite", + "distribution.ini", +] + +["test_PUIU_batchUpdatesForNode.js"] + +["test_PUIU_setCharsetForPage.js"] + +["test_PUIU_title_difference_spotter.js"] + +["test_browserGlue_bookmarkshtml.js"] + +["test_browserGlue_corrupt.js"] + +["test_browserGlue_corrupt_nobackup.js"] + +["test_browserGlue_corrupt_nobackup_default.js"] + +["test_browserGlue_distribution.js"] + +["test_browserGlue_migrate.js"] + +["test_browserGlue_prefs.js"] + +["test_browserGlue_restore.js"] + +["test_clearHistory_shutdown.js"] + +["test_interactions_blocklist.js"] + +["test_invalid_defaultLocation.js"] |