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): Implemented PanelUI Classes and Tests #338

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ export GECKODRIVER_LOG = $(shell pwd)/results/geckodriver.log

BLACK_CHECK = black -l 90 --check --diff .
BLACK_FIX = black -l 90 .
MINIMUM_COVERAGE = 95
FOXPUPPET_TESTS = pytest -vvv --driver Firefox --cov --cov-fail-under=$(MINIMUM_COVERAGE) --html results/report.html

check: install_poetry lint test

Expand All @@ -18,7 +16,7 @@ install_poetry:
curl -sSL https://install.python-poetry.org | python3 -

test: install_dependencies
poetry run $(FOXPUPPET_TESTS)
poetry run pytest

lint: install_dependencies
poetry run $(BLACK_CHECK)
Expand Down
7 changes: 6 additions & 1 deletion foxpuppet/windows/browser/navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from selenium.webdriver.common.by import By
from foxpuppet.region import Region
from foxpuppet.windows.base import BaseWindow
from foxpuppet.windows.browser.urlbar import UrlBar


class NavBar(Region):
Expand Down Expand Up @@ -39,3 +39,8 @@ def is_tracking_shield_displayed(self) -> bool:
return el.get_attribute("active") is not None
el = self.root.find_element(By.ID, "tracking-protection-icon")
return bool(el.get_attribute("state"))

@property
def url_bar(self) -> UrlBar:
"""Returns an instance of the UrlBar class."""
return UrlBar(self.window, self.root)
4 changes: 4 additions & 0 deletions foxpuppet/windows/browser/panel_ui/__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 Panel UI API and supporting files."""
200 changes: 200 additions & 0 deletions foxpuppet/windows/browser/panel_ui/panel_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Contains classes for handling Firefox Panel UI (Hamburger menu)."""

from selenium.webdriver.common.by import By

from foxpuppet.windows.browser.navbar import NavBar
from selenium.webdriver.remote.webelement import WebElement
from typing import Type, Any, TYPE_CHECKING, Optional
from selenium.webdriver.support import expected_conditions as EC


class PanelUI(NavBar):
"""Handles interaction with Panel UI."""

if TYPE_CHECKING:
from foxpuppet.windows import BrowserWindow

@staticmethod
def create(
window: Optional["BrowserWindow"], root: WebElement
) -> Type["PanelUI"] | Any:
"""Create a Panel UI object.

Args:
window (:py:class:`BrowserWindow`): Window object this region
appears in.
root
(:py:class:`~selenium.webdriver.remote.webelement.WebElement`):
WebDriver element object that serves as the root for the
Panel UI.

Returns:
:py:class:`PanelUI`: Firefox Panel UI.

"""
panel_items: dict = {}
_id: str | bool | WebElement | dict = root.get_property("id")

panel_items.update(PANEL_ITEMS)
return panel_items.get(_id, PanelUI)(window, root)

@property
def is_update_available(self) -> bool:
"""
Checks if the Panel UI button indicates a pending Firefox update.

Returns:
bool: True if an update notification (barge) is present, False otherwise.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
update_status = self.selenium.find_element(
*PanelUILocators.PANEL_UI_BUTTON
).get_attribute("barged")
return update_status == "true"

def open_panel_menu(self) -> None:
"""
Opens the Panel UI menu.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.PANEL_UI_BUTTON).click()
self.wait.until(
EC.presence_of_element_located(*PanelUILocators.PANEL_POPUP),
message="Panel UI menu did not open",
)

def open_new_tab(self) -> None:
"""
Opens a new tab using the Panel UI menu.
"""
initial_handles = set(self.selenium.window_handles)
self.open_panel_menu()
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.NEW_TAB).click()
self.wait.until(
lambda _: set(self.selenium.window_handles) - initial_handles,
message="New Tab did not open",
)
new_tab = (set(self.selenium.window_handles) - initial_handles).pop()
self.selenium.switch_to.window(new_tab)

def open_new_window(self) -> None:
"""
Opens a new window using the Panel UI menu.
"""
initial_handles = set(self.selenium.window_handles)
self.open_panel_menu()
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.NEW_WINDOW).click()
self.wait.until(
lambda _: set(self.selenium.window_handles) - initial_handles,
message="New window did not open",
)
new_window = (set(self.selenium.window_handles) - initial_handles).pop()
self.selenium.switch_to.window(new_window)

def open_private_window(self) -> None:
"""
Opens a new window in private browsing mode using the Panel UI menu.
"""
initial_handles = set(self.selenium.window_handles)
self.open_panel_menu()
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.PRIVATE_WINDOW).click()
self.wait.until(
lambda _: set(self.selenium.window_handles) - initial_handles,
message="Private window did not open",
)
from foxpuppet.windows.browser.window import BrowserWindow

new_private_window = self.selenium.window_handles[-1]
try:
private_window = BrowserWindow(
self.selenium, new_private_window
).is_private
if private_window:
self.selenium.switch_to.window(new_private_window)
except Exception as e:
raise Exception(f"The new window is not private: {str(e)}")

def open_history_menu(self) -> None:
"""
Opens the History in Panel UI Menu
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.HISTORY).click()
self.wait.until(
lambda _: self.selenium.find_element(
*PanelUILocators.PANEL_HISTORY
).is_displayed(),
message="History menu did not open",
)


class History(PanelUI):
def history_items(self) -> list[WebElement]:
"""
Retrieves all history items from the Panel UI history menu.

Returns:
list[WebElement]: List of WebElement objects representing history items.
Returns an empty list if no history items are found.
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
history_items = self.selenium.find_elements(
*PanelUILocators.RECENT_HISTORY_ITEMS
)
return history_items

def clear_history(self):
"""
Clears the browsing history.
"""
Comment on lines +152 to +155
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 we can confirm the history is cleared some way. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the history_items method

with self.selenium.context(self.selenium.CONTEXT_CHROME):
self.selenium.find_element(*PanelUILocators.CLEAR_RECENT_HISTORY).click()
self.selenium.switch_to.frame(
self.selenium.find_element(*PanelUILocators.HISTORY_IFRAME)
)
with self.selenium.context(self.selenium.CONTEXT_CONTENT):
self.selenium.find_element(*PanelUILocators.DROPDOWN_HISTORY).click()
self.selenium.find_element(
*PanelUILocators.CLEAR_HISTORY_EVERYTHING
).click()
self.selenium.execute_script(
"""
const shadowHost = arguments[0];
const shadowRoot = shadowHost.shadowRoot;
const clearRecentHistoryButton = shadowRoot.querySelector('button[dlgtype="accept"]');
clearRecentHistoryButton.click();
""",
self.selenium.find_element(*PanelUILocators.HISTORY_DIALOG_BUTTON),
)


class PanelUILocators:
CLEAR_HISTORY_EVERYTHING = (By.CSS_SELECTOR, "menuitem[value='0']")
CLEAR_RECENT_HISTORY = (By.ID, "appMenuClearRecentHistory")
CLEAR_RECENT_HISTORY_BUTTON = (By.CSS_SELECTOR, "button[dlgtype='accept']")
DROPDOWN_HISTORY = (By.ID, "sanitizeDurationChoice")
HISTORY = (By.ID, "appMenu-history-button")
HISTORY_DIALOG_BUTTON = (By.CSS_SELECTOR, "dialog[defaultButton='accept']")
HISTORY_IFRAME = (By.CSS_SELECTOR, "browser.dialogFrame")
NEW_TAB = (By.ID, "appMenu-new-tab-button2")
NEW_WINDOW = (By.ID, "appMenu-new-window-button2")
PANEL_HISTORY = (By.ID, "PanelUI-history")
PANEL_POPUP = ((By.ID, "appMenu-popup"),)
PANEL_UI_BUTTON = (By.ID, "PanelUI-menu-button")
PRIVATE_WINDOW = (By.ID, "appMenu-new-private-window-button2")
RECENT_HISTORY_ITEMS = (
By.CSS_SELECTOR,
"#appMenu_historyMenu toolbarbutton.subviewbutton",
)


PANEL_ITEMS = {
"PanelUI-menu-button": PanelUI,
"appMenu-history-button": History,
}
49 changes: 49 additions & 0 deletions foxpuppet/windows/browser/urlbar.py
Original file line number Diff line number Diff line change
@@ -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/.
"""Creates Navbar object to interact with Firefox URL Bar."""

from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from foxpuppet.region import Region


class UrlBar(Region):
def suggestions(self, url: str) -> list[str]:
"""
Get all URL suggestions shown in the URL bar.

Args:
url (str): The URL to type into the URL bar

Returns:
list[str]: List of suggested URLs that appear in the URL bar
"""
with self.selenium.context(self.selenium.CONTEXT_CHROME):
url_bar = self.selenium.find_element(*URLBarLocators.INPUT_FIELD)
url_bar.clear()
url_bar.send_keys(url)

self.wait.until(
lambda _: self.selenium.find_elements(*URLBarLocators.SEARCH_RESULTS)
)

search_results = self.selenium.find_elements(
*URLBarLocators.SEARCH_RESULT_ITEMS
)

suggested_urls = [
result.find_element(*URLBarLocators.SEARCH_RESULT_ITEM).text
for result in search_results
if result.find_element(*URLBarLocators.SEARCH_RESULT_ITEM).text
]

return suggested_urls


class URLBarLocators:
INPUT_FIELD = (By.ID, "urlbar-input")
SEARCH_RESULTS = (By.ID, "urlbar-results")
SEARCH_RESULT_ITEM = (By.CSS_SELECTOR, "span.urlbarView-url")
SEARCH_RESULT_ITEMS = (By.CSS_SELECTOR, "div.urlbarView-row[role='presentation']")
56 changes: 55 additions & 1 deletion foxpuppet/windows/browser/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@
from foxpuppet.windows.browser.navbar import NavBar
from foxpuppet.windows.browser.notifications import BaseNotification
from foxpuppet.windows.browser.bookmarks.bookmark import Bookmark
from foxpuppet.windows.browser.panel_ui.panel_ui import PanelUI
from selenium.webdriver.remote.webelement import WebElement
from typing import Any, Optional, Union, TypeVar, Type

T = TypeVar("T", bound="BaseNotification")
P = TypeVar("P", bound="PanelUI")


class BrowserWindow(BaseWindow):
"""Representation of a browser window."""

_bookmark_locator = (By.ID, "main-window") # editBookmarkPanelTemplate
_bookmark_locator = (By.ID, "main-window")
_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")
_nav_bar_locator = (By.ID, "nav-bar")
_notification_locator = (By.CSS_SELECTOR, "#notification-popup popupnotification")
_panel_ui_locator = (By.ID, "PanelUI-menu-button")
_app_menu_notification_locator = (
By.CSS_SELECTOR,
"#appMenu-notification-popup popupnotification",
)
_app_menu_panel_ui_locator = (
By.CSS_SELECTOR,
"#appMenu-mainView .panel-subview-body toolbarbutton",
)
_tab_browser_locator = (By.ID, "tabbrowser-tabs")

@property
Expand Down Expand Up @@ -81,6 +88,23 @@ def bookmark(self) -> Bookmark:
root = self.selenium.find_element(*self._bookmark_locator)
return Bookmark.create(self, root)

@property
def panel(self) -> PanelUI | Any:
panel_root = None
with self.selenium.context(self.selenium.CONTEXT_CHROME):
root = self.selenium.find_element(*self._panel_ui_locator)
panel_root = PanelUI.create(self, root)

panel_items = self.selenium.find_elements(*self._app_menu_panel_ui_locator)
for item in panel_items:
_id = item.get_property("id")
from foxpuppet.windows.browser.panel_ui.panel_ui import PANEL_ITEMS

if _id in PANEL_ITEMS and item.is_displayed():
panel_root = PANEL_ITEMS[_id].create(self, item) # type: ignore

return panel_root

def wait_for_notification(
self,
notification_class: Optional[Type[T]] = BaseNotification, # type: ignore
Expand Down Expand Up @@ -129,6 +153,36 @@ def wait_for_bookmark(self) -> Bookmark:
)
return self.bookmark

def wait_for_panel(
self, panel_ui_class: Optional[Type[P]] = PanelUI # type: ignore
) -> Optional[P]:
"""Wait for the specified PanelUI item to be displayed.

Args:
panel_ui_class (:py:class:`PanelUI`, optional):
The PanelUI subclass to wait for. If `None` is specified, it
will wait for any panel UI to be displayed. Defaults to `PanelUI`.

Returns:
Optional[:py:class:`PanelUI`]: The displayed PanelUI or `None` if not found.
"""
if panel_ui_class:
if panel_ui_class is PanelUI:
message = "No panel UI was shown."
else:
message = f"{panel_ui_class.__name__} was not shown."
self.wait.until(
lambda _: isinstance(self.panel, panel_ui_class),
message=message,
)
return self.panel # type: ignore
else:
self.wait.until(
lambda _: self.panel is None,
message="Unexpected panel UI was shown.",
)
return None

@property
def is_private(self) -> bool | Any:
"""Property that checks if the specified window is private or not.
Expand Down
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
addopts = -vvv --driver Firefox --cov --cov-fail-under=95 --html=results/report.html --self-contained-html
testpaths = tests
Comment on lines +2 to +3
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a newline

Loading