Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type(feat): Implement Bookmark Classes and Tests #327

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions foxpuppet/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.action_chains import ActionChains
from foxpuppet.windows import BaseWindow


Expand Down Expand Up @@ -38,3 +39,4 @@ def __init__(self, window: BaseWindow, root: WebElement):
self.selenium: WebDriver = window.selenium
self.wait: WebDriverWait = window.wait
self.window: BaseWindow = window
self.actions: ActionChains = ActionChains(self.selenium)
4 changes: 4 additions & 0 deletions foxpuppet/windows/browser/bookmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 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 the bookmarks interaction API and supporting files."""
211 changes: 211 additions & 0 deletions foxpuppet/windows/browser/bookmarks/bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# 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.common.exceptions import NoSuchElementException
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]
Comment on lines +14 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏾



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).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()
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is overly complex? Does it matter how a bookmark is deleted?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I added the more detailed approach is because if we test adding a bookmark using the more detailed approach, then we should do same with the delete.


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).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.ID, "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, "navigator-toolbox")
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")
7 changes: 5 additions & 2 deletions foxpuppet/windows/browser/navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"""Creates Navbar object to interact with Firefox Navigation Bar."""

from selenium.webdriver.common.by import By

from foxpuppet.region import Region
from foxpuppet.windows.base import BaseWindow


class NavBar(Region):
Expand All @@ -20,7 +20,10 @@ class NavBar(Region):

"""

_tracking_protection_shield_locator = (By.ID, "tracking-protection-icon-box")
_tracking_protection_shield_locator = (
By.ID,
"tracking-protection-icon-box",
)

@property
def is_tracking_shield_displayed(self) -> bool:
Expand Down
29 changes: 29 additions & 0 deletions foxpuppet/windows/browser/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from foxpuppet.windows import BaseWindow
from foxpuppet.windows.browser.navbar import NavBar
from foxpuppet.windows.browser.notifications import BaseNotification
from foxpuppet.windows.browser.bookmarks.bookmark import Bookmark
from selenium.webdriver.remote.webelement import WebElement
from typing import Any, Optional, Union, TypeVar, Type

Expand All @@ -19,6 +20,7 @@
class BrowserWindow(BaseWindow):
"""Representation of a browser window."""

_bookmark_locator = (By.ID, "main-window") # editBookmarkPanelTemplate
_file_menu_button_locator = (By.ID, "file-menu")
_file_menu_private_window_locator = (By.ID, "menu_newPrivateWindow")
_file_menu_new_window_button_locator = (By.ID, "menu_newNavigator")
Expand Down Expand Up @@ -67,6 +69,18 @@ def notification(self) -> BaseNotification | Any:
pass
return None # no notification is displayed

@property
def bookmark(self) -> Bookmark:
"""Provide access to the currently displayed bookmark.

Returns:
:py:class:`BaseBookmark`: FoxPuppet BasicBookmark object.

"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
root = self.selenium.find_element(*self._bookmark_locator)
return Bookmark.create(self, root)

def wait_for_notification(
self,
notification_class: Optional[Type[T]] = BaseNotification, # type: ignore
Expand Down Expand Up @@ -100,6 +114,21 @@ def wait_for_notification(
)
return None

def wait_for_bookmark(self) -> Bookmark:
"""Wait for the bookmark panel to be displayed.

Returns:
Optional[Bookmark]: The Bookmark object if found, or None if not found.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
message = "Bookmark panel was not shown."

self.wait.until(
lambda _: self.bookmark is not None,
message=message,
)
return self.bookmark

@property
def is_private(self) -> bool | Any:
"""Property that checks if the specified window is private or not.
Expand Down
Loading
Loading