foxpuppet/windows/browser/bookmarks/bookmark.py (156 lines of code) (raw):
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Contains classes for handling Firefox bookmarks."""
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.keys import Keys
from foxpuppet.windows.browser.navbar import NavBar
from typing import TYPE_CHECKING, Optional, TypedDict, List
class BookmarkData(TypedDict):
"""Bookmark properties."""
name: str
url: str
tags: Optional[List[str]]
keyword: Optional[str]
class Bookmark(NavBar):
"""Handles Bookmark operations in Firefox."""
if TYPE_CHECKING:
from foxpuppet.windows.browser.window import BrowserWindow
@staticmethod
def create(window: "BrowserWindow", root: WebElement) -> "Bookmark":
"""Create a bookmark object.
Args:
window (:py:class:`BrowserWindow`): Window object this bookmark appears in
root (:py:class:`~selenium.webdriver.remote.webelement.WebElement`): WebDriver element object for bookmark
Returns:
:py:class:`Bookmark`: Bookmark instance
"""
with window.selenium.context(window.selenium.CONTEXT_CHROME):
return Bookmark(window, root)
@property
def is_bookmarked(self) -> bool:
"""Checks if the current page is bookmarked using the star button.
Returns:
bool: True if the page is bookmarked, False otherwise.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
star_button_image = self.selenium.find_element(
*BookmarkLocators.STAR_BUTTON_IMAGE
)
return star_button_image.get_attribute("starred") == "true"
def add_bookmark(
self, bookmark_data: Optional[BookmarkData] = None, is_detailed: bool = False
) -> None:
"""
Add a bookmark using either quick add (star button) or detailed menu approach.
Args:
detailed (bool, optional): Whether to use detailed menu approach. Defaults to False.
bookmark_data (BookmarkData, optional): Data for the bookmark when using detailed menu.
Required when detailed is True.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
if not is_detailed:
self.selenium.find_element(*BookmarkLocators.STAR_BUTTON_IMAGE).click()
self.selenium.find_element(*BookmarkLocators.FOLDER_MENU).click()
self.selenium.find_element(*BookmarkLocators.OTHER_BOOKMARKS_STAR).click()
self.selenium.find_element(*BookmarkLocators.SAVE_BUTTON).click()
else:
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.actions.context_click(
self.selenium.find_element(*BookmarkLocators.NAVIGATOR_TOOLBOX)
).perform()
WebDriverWait(self.selenium, 10).until(
EC.presence_of_element_located(BookmarkLocators.MENU_BAR)
)
self.selenium.find_element(*BookmarkLocators.MENU_BAR).click()
self.selenium.find_element(
*BookmarkLocators.MAIN_MENU_BOOKMARK
).click()
self.actions.context_click(
self.selenium.find_element(*BookmarkLocators.MANAGE_BOOKMARKS)
).perform()
self.selenium.find_element(*BookmarkLocators.ADD_BOOKMARK).click()
bookmark_frame = self.selenium.find_element(
*BookmarkLocators.ADD_BOOKMARK_FRAME
)
self.selenium.switch_to.frame(bookmark_frame)
if bookmark_data:
if bookmark_data["name"]:
self.actions.send_keys(bookmark_data["name"]).perform()
self.actions.send_keys(Keys.TAB).perform()
if bookmark_data["url"]:
self.actions.send_keys(
bookmark_data["url"] + Keys.TAB
).perform()
if (tags := bookmark_data["tags"]) is not None:
for tag in tags:
self.actions.send_keys(tag).perform()
self.actions.send_keys(",").perform()
self.actions.send_keys(Keys.TAB).perform()
if bookmark_data.get("keyword"):
keyword = bookmark_data["keyword"] or ""
self.actions.send_keys(keyword + Keys.TAB).perform()
self.actions.send_keys(
Keys.TAB, Keys.TAB, Keys.TAB, Keys.ENTER
).perform()
if folder := self.selenium.find_element(
*BookmarkLocators.BOOKMARK_FOLDER
):
folder.click()
self.selenium.switch_to.frame(folder)
self.actions.send_keys(Keys.TAB, Keys.ENTER).perform()
def bookmark_exists(self, label: str) -> bool:
"""
Check if a bookmark with the given label exists.
Args:
label (str): The name of the bookmark to search for.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*BookmarkLocators.PANEL_MENU).click()
self.selenium.find_element(*BookmarkLocators.PANEL_BOOKMARK_MENU).click()
panel_bookmarks = self.selenium.find_element(
*BookmarkLocators.PANEL_BOOKMARK_TOOLBAR
)
menu_items = panel_bookmarks.find_elements(
By.CSS_SELECTOR, "toolbarbutton.bookmark-item"
)
if any(
label.lower() in item_label.lower()
for item in menu_items
if (item_label := item.get_attribute("label"))
):
return True
return False
def delete_bookmark(
self, label: Optional[str] = None, is_detailed: bool = False
) -> None:
"""
Delete a bookmark using either quick delete (star button) or detailed menu approach.
Args:
detailed (bool, optional): Whether to use detailed menu approach. Defaults to False.
label (str, optional): Label of the bookmark to delete when using detailed approach.
Required when detailed is True.
Returns:
bool: True if bookmark was successfully deleted (always True for detailed approach)
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
if not is_detailed:
star_button_image = self.selenium.find_element(
*BookmarkLocators.STAR_BUTTON_IMAGE
)
if (
star_button_image
and star_button_image.get_attribute("starred") == "true"
):
self.selenium.find_element(
*BookmarkLocators.STAR_BUTTON_IMAGE
).click()
self.selenium.find_element(*BookmarkLocators.REMOVE_BUTTON).click()
return
self.actions.context_click(
self.selenium.find_element(*BookmarkLocators.NAVIGATOR_TOOLBOX)
).perform()
self.selenium.find_element(*BookmarkLocators.MENU_BAR).click()
bookmark_menu = self.selenium.find_element(
*BookmarkLocators.MAIN_MENU_BOOKMARK
)
self.selenium.find_element(*BookmarkLocators.MAIN_MENU_BOOKMARK).click()
menu_item = bookmark_menu.find_element(
By.CSS_SELECTOR, f"menuitem.bookmark-item[label='{label}']"
)
self.actions.context_click(menu_item).perform()
self.selenium.find_element(*BookmarkLocators.DELETE_MENU_ITEM).click()
class BookmarkLocators:
ADD_BOOKMARK = (By.ID, "placesContext_new:bookmark")
ADD_BOOKMARK_FRAME = (By.CSS_SELECTOR, "browser[class='dialogFrame']")
BOOKMARK_FOLDER = (
By.CSS_SELECTOR,
"browser.dialogFrame[name='dialogFrame-window-modal-dialog-subdialog']",
)
BOOKMARK_PROPERTIES_DIALOG = (By.ID, "bookmarkproperties")
DELETE_MENU_ITEM = (By.ID, "placesContext_deleteBookmark")
FOLDER_MENU = (
By.CSS_SELECTOR,
"#editBookmarkPanelContent .editBMPanel_folderRow #editBMPanel_folderMenuList",
)
MAIN_MENU_BOOKMARK = (By.ID, "bookmarksMenu")
MANAGE_BOOKMARKS = (By.ID, "bookmarksShowAll")
MENU_BAR = (By.ID, "toggle_toolbar-menubar")
NAME_FIELD = (By.ID, "editBMPanel_namePicker")
NAVIGATOR_TOOLBOX = (By.ID, "TabsToolbar")
OTHER_BOOKMARKS = (By.ID, "OtherBookmarks")
OTHER_BOOKMARKS_STAR = (By.ID, "editBMPanel_unfiledRootItem")
PANEL_BOOKMARK_MENU = (By.ID, "appMenu-bookmarks-button")
PANEL_BOOKMARK_TOOLBAR = (By.ID, "panelMenu_bookmarksMenu")
PANEL_MENU = (By.ID, "PanelUI-menu-button")
REMOVE_BUTTON = (By.ID, "editBookmarkPanelRemoveButton")
SAVE_BOOKMARK = (By.CSS_SELECTOR, 'button[dlgtype="accept"][label="Save"]')
SAVE_BUTTON = (By.ID, "editBookmarkPanelDoneButton")
STAR_BUTTON = (By.ID, "star-button-box")
STAR_BUTTON_IMAGE = (By.ID, "star-button")
TOOLBAR_CONTEXT_MENU = (By.ID, "toolbar-context-menu")