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

Conversation

Temidayo32
Copy link
Collaborator

This PR add functionalities to set, unset, and retrieve bookmarks

Because

  • Extend FoxPuppet by implementing the Bookmark functionality

This commit

  • Add functionalities (classes and tests) to set, unset and retrieve bookmarks.

Fixes #312

@Temidayo32 Temidayo32 changed the title Implement Bookmark Classes and Tests type(feat): Implement Bookmark Classes and Tests Dec 13, 2024
@Temidayo32
Copy link
Collaborator Author

@b4handjr Current tests didn't meet 95% requirement hence the failure. I will add missing tests to meet the requirements.

This PR add functionalities to set, unset, and retrieve bookmarks
Comment on lines 103 to 105
class AdvancedBookmark(BasicBookmark):
"""Handles advanced bookmark operations."""

Copy link
Contributor

Choose a reason for hiding this comment

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

What's an "Advanced Bookmark" exactly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, so in Desktop Smoke test, a similar term is used so I decided to adopt it in FoxPuppet. Basically, an advanced bookmark is a more detailed way of adding a bookmark, which involves name, url, tags, and keywords, as against just simply using the url alone when creating a Basic Bookmark with the star button.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay let me ask Ben C for clarifications

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

@ben-c-at-moz Can you explain what your "Advanced Bookmark" action does exactly?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is maybe not the best name--the function here is "advanced" only in that it adds a bookmark through the "Other bookmarks" option in the Library window. The bookmark itself should be normal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I agree, the name is a little confusing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But I'm fine if we just want to keep using it for more detailed bookmark operations, as long as we explain it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we just call it Bookmarks and put all the functionality into one class? If a user wants to do it through the "Other Bookmarks" option, we can just have a flag or something when they create the bookmark.

@Temidayo32 Temidayo32 force-pushed the bookmark branch 2 times, most recently from 93e63d3 to 6f591c7 Compare December 17, 2024 14:34
Copy link
Collaborator

@ben-c-at-moz ben-c-at-moz left a comment

Choose a reason for hiding this comment

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

I think this is good, but I'd like it to pass all checks before merging.

Comment on lines 103 to 105
class AdvancedBookmark(BasicBookmark):
"""Handles advanced bookmark operations."""

Copy link
Collaborator

Choose a reason for hiding this comment

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

But I'm fine if we just want to keep using it for more detailed bookmark operations, as long as we explain it.

@Temidayo32
Copy link
Collaborator Author

Temidayo32 commented Dec 17, 2024

I think this is good, but I'd like it to pass all checks before merging.

@ben-c-at-moz Actually, it's failing in Nightly build due to this: browser-actions/setup-firefox#621

Copy link
Contributor

@b4handjr b4handjr left a comment

Choose a reason for hiding this comment

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

Had a quick look but I think I want the bookmark functionality all in 1 class.

Comment on lines 51 to 105

def click_element(self, locator: Tuple[str, str]) -> None:
"""Click on an element by its locator."""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.find_element(locator).click()

def find_element(self, locator: Tuple[str, str]) -> WebElement:
"""Find and return a web element by its locator."""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
try:
element = self.root.find_element(*locator)
return element
except Exception as e:
raise NoSuchElementException(
f"Error locating element with locator {locator}: {e}"
)

def context_click(self, locator: Tuple[str, str]) -> None:
"""
Perform a right-click (context-click) on an element.

Args:
locator (tuple): Locator for the element to right-click.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
element = self.find_element(locator)
self.actions.context_click(element).perform()

def switch_to_frame(self, locator: Tuple[str, str]) -> None:
"""
Switch to an iFrame using its locator

Args:
locator (tuple): Locator for the iFrame elemeent.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
frame_element = self.find_element(locator)
if frame_element is not None:
self.selenium.switch_to.frame(frame_element)
else:
raise NoSuchElementException(f"iFrame with locator {locator} not found.")

def switch_to_default_context(self) -> None:
"""Switch back to default context."""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.switch_to.default_content()

def open_main_menu(
self, locator_toolbar: Tuple[str, str], locator_menu_bar: Tuple[str, str]
) -> None:
"""Activate main menu bar"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.context_click(locator_toolbar)
self.click_element(locator_menu_bar)

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems out of scope for this PR no? Maybe I am missing something

Copy link
Collaborator Author

@Temidayo32 Temidayo32 Dec 18, 2024

Choose a reason for hiding this comment

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

These methods are used in Bookmarks. I added them here because the issue stated to extend the NavBar with Bookmark functionalities. But then, along the way, I realized Bookmarks doesn't exactly fit with the nav-bar element. While some bookmark web elements exist in the nav bar, others are scattered across different parts of the dom. I could move them all to Bookmark itself if that makes things clearer.

Add functionality to NavBar (and elsewhere) to set, unset, and retrieve bookmarks.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well some of these just seem like generic actions, like switch_to_default_context() and find_element.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will you suggest I move them to a class higher up the hierarchy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I moved all the generic actions into region.py

Copy link
Contributor

Choose a reason for hiding this comment

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

I think these should be removed if they aren't specifically related to bookmark functionality. We can add these later if need be.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually, these methods are functionalities I reused a lot in the Bookmark functionality. So instead of repeating them, I just factored them out into those methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand but a lot of them are doing what selenium has built in. Selenium has a find_element method already, as an example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@b4handjr I have refactored and removed all of them. It is now ready for another review.

Comment on lines 165 to 166
else:
raise ValueError(f"Unsupported bookmark class: {bookmark_class}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't we get this for free with the static typing we're adding to the definition?

Comment on lines 82 to 85
if bookmark_instance is None:
raise ValueError("Failed to get AdvancedBookmark instance.")
if not isinstance(bookmark_instance, AdvancedBookmark):
raise ValueError("Expected AdvancedBookmark instance but got different type")
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite like this logic in a test. Why would bookmark_instance ever be none after wait_for_bookmark?

The raise ValueError seems redundant as you currently have the wait_for_bookmark throwing a ValueError too.

I think we don't want to do instance checking here if we don't need to. We should assume that we get an AdvancedBookmark until proven that we don't.

Copy link
Collaborator Author

@Temidayo32 Temidayo32 Dec 18, 2024

Choose a reason for hiding this comment

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

I didn't have those lines in initially. So if I remove those lines, mypy will raise type errors like this. This is coming from the create method returning None instead of throwing the exception as you pointed out below.

tests/test_bookmarks.py:95: error: Item "BasicBookmark" of "BasicBookmark | None" has no attribute
"add_bookmark"  [union-attr]
        bookmark_instance.add_bookmark(bookmark_data)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests/test_bookmarks.py:95: error: Item "None" of "BasicBookmark | None" has no attribute "add_bookmark"
[union-attr]
        bookmark_instance.add_bookmark(bookmark_data)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests/test_bookmarks.py:96: error: Incompatible return value type (got "BasicBookmark | None", expected
"AdvancedBookmark")  [return-value]
        return bookmark_instance

I will do a different work around for this when I refactor it.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good error, but the solution isn't what you currently have. Looking forward to seeing how you figure this out

Comment on lines 40 to 44
with window.selenium.context(window.selenium.CONTEXT_CHROME):
try:
return BasicBookmark(window, root)
except NoSuchElementException:
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

I am curious as to why you'd like to return None and not throw the error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it might be less verbose. But, now again, it could confusing and unclear. I will just correct that.

@b4handjr
Copy link
Contributor

Let me know when you'd like me to look at this again.

@Temidayo32
Copy link
Collaborator Author

Let me know when you'd like me to look at this again.

Yeah, you can. Thanks.

@b4handjr
Copy link
Contributor

Let me know when you'd like me to look at this again.

Yeah, you can. Thanks.

Well I added a comment above too

@Temidayo32
Copy link
Collaborator Author

Hi @b4handjr The code is now ready for another review. Thanks.

Copy link
Contributor

@b4handjr b4handjr left a comment

Choose a reason for hiding this comment

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

Lots of great work here, this is really coming together! I would still like to see some consolidation and clarification with it comes to adding or deleting a bookmark. Also, there are some isinstance checks that I think aren't needed as we are using types to verify that.

return True
return False

def add(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference with add() or add_bookmark()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I should probably use a flag instead and combine both into one method so it isn't confusing

Comment on lines 125 to 129
if 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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Think you could use the walrus operator here? https://peps.python.org/pep-0572/

Comment on lines 153 to 154
if panel_bookmarks:
menu_items = panel_bookmarks.find_elements(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you check if the panel_bookmarks is true? Wouldn't selenium throw an error if it couldn't find the element needed?

Comment on lines 158 to 161
for item in menu_items:
item_label = item.get_attribute("label")
if item_label and label.lower() in item_label.lower():
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

This could probably be done with a list comprehension.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought to go with a generator expression here which might better fit, since the intention here is not to create a list, but to evaluate a condition.

Comment on lines 24 to 26
_tracking_protection_shield_locator: Tuple[str, str] = (
By.ID,
"tracking-protection-icon-box",
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Probably not

self.actions.send_keys(Keys.TAB, Keys.TAB, Keys.TAB, Keys.ENTER).perform()
self.actions.send_keys(Keys.TAB, Keys.ENTER).perform()

self.selenium.switch_to.default_content()
Copy link
Contributor

Choose a reason for hiding this comment

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

Using the with clause, we don't need to switch back to default content here.

Comment on lines 110 to 115
if frame_element is not None:
self.selenium.switch_to.frame(frame_element)
else:
raise NoSuchElementException(
f"iFrame with locator {BookmarkLocators.ADD_BOOKMARK_FRAME} not found."
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Selenium will raise an error before the program gets here, if you want to raise an error with specific context you'll have to use a try/except block.

from foxpuppet.windows import BaseWindow
from typing import Tuple
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't this this is used in here?

@@ -6,7 +6,10 @@
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 selenium.common.exceptions import NoSuchElementException
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't this this is used in here?

@Temidayo32 Temidayo32 force-pushed the bookmark branch 5 times, most recently from a831f09 to 49ecba8 Compare December 27, 2024 14:30
Copy link
Contributor

@b4handjr b4handjr left a comment

Choose a reason for hiding this comment

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

Looking great, nice and pointed towards the bookmarks. Just had some follow up points I wanted to mention.

).perform()
self.selenium.find_element(*BookmarkLocators.ADD_BOOKMARK).click()

frame_element = self.selenium.find_element(
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we name this bookmark_frame or something like that?

Comment on lines 91 to 92
if bookmark_data is not None:
if bookmark_data["name"]:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you could use the walrus operator here, and it would allow bookmark_data to be used bellow as well.

Comment on lines 114 to 118
if (
folder := self.selenium.find_element(
*BookmarkLocators.BOOKMARK_FOLDER
)
) is not None:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to do this None checks. You can remove the is not None part and do the same check as well. Also, good use of the walrus operator!

Comment on lines 123 to 124
def retrieve_bookmark(self, label: str) -> bool:
"""
Check if a bookmark with the given label exists.

Copy link
Contributor

Choose a reason for hiding this comment

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

The function name reads as if this will return a bookmark but it seems to be more of a check. Can you update the name to reflect that?

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.

Comment on lines 162 to 171
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()
else:
self.actions.context_click(
Copy link
Contributor

Choose a reason for hiding this comment

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

For operations like this where you have just 1 condition to check you can just have an if with the expression you want to evaluate ending with a return and then outside of that if block you can add the actions that will be done if the expression evaluates opposing to what you wanted.

So something like

if True:
    code here if true
    return
code here if false

Does that make sense?

Comment on lines +14 to +20
class BookmarkData(TypedDict):
"""Bookmark properties."""

name: str
url: str
tags: Optional[List[str]]
keyword: Optional[str]
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏾

@Temidayo32 Temidayo32 force-pushed the bookmark branch 3 times, most recently from 587f95f to 1c42add Compare January 3, 2025 20:50
@b4handjr b4handjr requested a review from ben-c-at-moz January 7, 2025 15:38
Copy link
Contributor

@b4handjr b4handjr left a comment

Choose a reason for hiding this comment

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

R+ great work as always!! 🎉 🚀

Moved all bookmark functionalities into a single class, Bookmark
@b4handjr b4handjr merged commit 2a76bbb into mozilla:main Jan 13, 2025
7 checks passed
@Temidayo32 Temidayo32 deleted the bookmark branch February 20, 2025 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Bookmark Functionality
3 participants