Skip to content

Commit 6783fd2

Browse files
Temidayo32b4handjr
andauthored
type(feat): Implemented PanelUI Classes and Tests (#338)
Implemented PanelUI Classes and Tests * Extended Panel UI methods and tests * Added History SubClass * Removed FoxPuppet's unrelated tests * Adjusted Panel UI methods * Added pytest.ini * Added Private Window Test and Nit fixes * fixed private window method * Removed time.sleep --------- Co-authored-by: Benjamin Forehand Jr <bennyjr169@gmail.com>
1 parent 3d9272f commit 6783fd2

File tree

8 files changed

+483
-5
lines changed

8 files changed

+483
-5
lines changed

Makefile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ export GECKODRIVER_LOG = $(shell pwd)/results/geckodriver.log
33

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

97
check: install_poetry lint test
108

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

2018
test: install_dependencies
21-
poetry run $(FOXPUPPET_TESTS)
19+
poetry run pytest
2220

2321
lint: install_dependencies
2422
poetry run $(BLACK_CHECK)

foxpuppet/windows/browser/navbar.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from selenium.webdriver.common.by import By
77
from foxpuppet.region import Region
8-
from foxpuppet.windows.base import BaseWindow
8+
from foxpuppet.windows.browser.urlbar import UrlBar
99

1010

1111
class NavBar(Region):
@@ -39,3 +39,8 @@ def is_tracking_shield_displayed(self) -> bool:
3939
return el.get_attribute("active") is not None
4040
el = self.root.find_element(By.ID, "tracking-protection-icon")
4141
return bool(el.get_attribute("state"))
42+
43+
@property
44+
def url_bar(self) -> UrlBar:
45+
"""Returns an instance of the UrlBar class."""
46+
return UrlBar(self.window, self.root)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
# You can obtain one at http://mozilla.org/MPL/2.0/.
4+
"""Contains the Panel UI API and supporting files."""
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
# You can obtain one at http://mozilla.org/MPL/2.0/.
4+
"""Contains classes for handling Firefox Panel UI (Hamburger menu)."""
5+
6+
from selenium.webdriver.common.by import By
7+
import time
8+
from foxpuppet.windows.browser.navbar import NavBar
9+
from selenium.webdriver.remote.webelement import WebElement
10+
from typing import Type, Any, TYPE_CHECKING, Optional
11+
from selenium.webdriver.support import expected_conditions as EC
12+
13+
14+
class PanelUI(NavBar):
15+
"""Handles interaction with Panel UI."""
16+
17+
if TYPE_CHECKING:
18+
from foxpuppet.windows import BrowserWindow
19+
20+
@staticmethod
21+
def create(
22+
window: Optional["BrowserWindow"], root: WebElement
23+
) -> Type["PanelUI"] | Any:
24+
"""Create a Panel UI object.
25+
26+
Args:
27+
window (:py:class:`BrowserWindow`): Window object this region
28+
appears in.
29+
root
30+
(:py:class:`~selenium.webdriver.remote.webelement.WebElement`):
31+
WebDriver element object that serves as the root for the
32+
Panel UI.
33+
34+
Returns:
35+
:py:class:`PanelUI`: Firefox Panel UI.
36+
37+
"""
38+
panel_items: dict = {}
39+
_id: str | bool | WebElement | dict = root.get_property("id")
40+
41+
panel_items.update(PANEL_ITEMS)
42+
return panel_items.get(_id, PanelUI)(window, root)
43+
44+
@property
45+
def is_update_available(self) -> bool:
46+
"""
47+
Checks if the Panel UI button indicates a pending Firefox update.
48+
49+
Returns:
50+
bool: True if an update notification (barge) is present, False otherwise.
51+
"""
52+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
53+
update_status = self.selenium.find_element(
54+
*PanelUILocators.PANEL_UI_BUTTON
55+
).get_attribute("barged")
56+
return update_status == "true"
57+
58+
def open_panel_menu(self) -> None:
59+
"""
60+
Opens the Panel UI menu.
61+
"""
62+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
63+
self.selenium.find_element(*PanelUILocators.PANEL_UI_BUTTON).click()
64+
self.wait.until(
65+
EC.presence_of_element_located(*PanelUILocators.PANEL_POPUP),
66+
message="Panel UI menu did not open",
67+
)
68+
69+
def open_new_tab(self) -> None:
70+
"""
71+
Opens a new tab using the Panel UI menu.
72+
"""
73+
initial_handles = set(self.selenium.window_handles)
74+
self.open_panel_menu()
75+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
76+
self.selenium.find_element(*PanelUILocators.NEW_TAB).click()
77+
self.wait.until(
78+
lambda _: set(self.selenium.window_handles) - initial_handles,
79+
message="New Tab did not open",
80+
)
81+
new_tab = (set(self.selenium.window_handles) - initial_handles).pop()
82+
self.selenium.switch_to.window(new_tab)
83+
84+
def open_new_window(self) -> None:
85+
"""
86+
Opens a new window using the Panel UI menu.
87+
"""
88+
initial_handles = set(self.selenium.window_handles)
89+
self.open_panel_menu()
90+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
91+
self.selenium.find_element(*PanelUILocators.NEW_WINDOW).click()
92+
self.wait.until(
93+
lambda _: set(self.selenium.window_handles) - initial_handles,
94+
message="New window did not open",
95+
)
96+
new_window = (set(self.selenium.window_handles) - initial_handles).pop()
97+
self.selenium.switch_to.window(new_window)
98+
self.wait.until(
99+
lambda _: self.selenium.execute_script("return document.readyState")
100+
== "complete",
101+
message="New window document not fully loaded",
102+
)
103+
104+
def open_private_window(self) -> None:
105+
"""
106+
Opens a new window in private browsing mode using the Panel UI menu.
107+
"""
108+
initial_handles = set(self.selenium.window_handles)
109+
self.open_panel_menu()
110+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
111+
self.selenium.find_element(*PanelUILocators.PRIVATE_WINDOW).click()
112+
self.wait.until(
113+
lambda _: set(self.selenium.window_handles) - initial_handles,
114+
message="Private window did not open",
115+
)
116+
from foxpuppet.windows.browser.window import BrowserWindow
117+
118+
new_private_window = self.selenium.window_handles[-1]
119+
try:
120+
private_window = BrowserWindow(
121+
self.selenium, new_private_window
122+
).is_private
123+
if private_window:
124+
self.selenium.switch_to.window(new_private_window)
125+
except Exception as e:
126+
raise Exception(f"The new window is not private: {str(e)}")
127+
128+
def open_history_menu(self) -> None:
129+
"""
130+
Opens the History in Panel UI Menu
131+
"""
132+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
133+
self.selenium.find_element(*PanelUILocators.HISTORY).click()
134+
self.wait.until(
135+
lambda _: self.selenium.find_element(
136+
*PanelUILocators.PANEL_HISTORY
137+
).is_displayed(),
138+
message="History menu did not open",
139+
)
140+
141+
142+
class History(PanelUI):
143+
def history_items(self) -> list[WebElement]:
144+
"""
145+
Retrieves all history items from the Panel UI history menu.
146+
147+
Returns:
148+
list[WebElement]: List of WebElement objects representing history items.
149+
Returns an empty list if no history items are found.
150+
"""
151+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
152+
history_items = self.selenium.find_elements(
153+
*PanelUILocators.RECENT_HISTORY_ITEMS
154+
)
155+
return history_items
156+
157+
def clear_history(self):
158+
"""
159+
Clears the browsing history.
160+
"""
161+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
162+
self.selenium.find_element(*PanelUILocators.CLEAR_RECENT_HISTORY).click()
163+
self.selenium.switch_to.frame(
164+
self.selenium.find_element(*PanelUILocators.HISTORY_IFRAME)
165+
)
166+
with self.selenium.context(self.selenium.CONTEXT_CONTENT):
167+
self.selenium.find_element(*PanelUILocators.DROPDOWN_HISTORY).click()
168+
self.selenium.find_element(
169+
*PanelUILocators.CLEAR_HISTORY_EVERYTHING
170+
).click()
171+
self.selenium.execute_script(
172+
"""
173+
const shadowHost = arguments[0];
174+
const shadowRoot = shadowHost.shadowRoot;
175+
const clearRecentHistoryButton = shadowRoot.querySelector('button[dlgtype="accept"]');
176+
clearRecentHistoryButton.click();
177+
""",
178+
self.selenium.find_element(*PanelUILocators.HISTORY_DIALOG_BUTTON),
179+
)
180+
time.sleep(1)
181+
182+
183+
class PanelUILocators:
184+
CLEAR_HISTORY_EVERYTHING = (By.CSS_SELECTOR, "menuitem[value='0']")
185+
CLEAR_RECENT_HISTORY = (By.ID, "appMenuClearRecentHistory")
186+
CLEAR_RECENT_HISTORY_BUTTON = (By.CSS_SELECTOR, "button[dlgtype='accept']")
187+
DROPDOWN_HISTORY = (By.ID, "sanitizeDurationChoice")
188+
HISTORY = (By.ID, "appMenu-history-button")
189+
HISTORY_DIALOG_BUTTON = (By.CSS_SELECTOR, "dialog[defaultButton='accept']")
190+
HISTORY_IFRAME = (By.CSS_SELECTOR, "browser.dialogFrame")
191+
NEW_TAB = (By.ID, "appMenu-new-tab-button2")
192+
NEW_WINDOW = (By.ID, "appMenu-new-window-button2")
193+
PANEL_HISTORY = (By.ID, "PanelUI-history")
194+
PANEL_POPUP = ((By.ID, "appMenu-popup"),)
195+
PANEL_UI_BUTTON = (By.ID, "PanelUI-menu-button")
196+
PRIVATE_WINDOW = (By.ID, "appMenu-new-private-window-button2")
197+
RECENT_HISTORY_ITEMS = (
198+
By.CSS_SELECTOR,
199+
"#appMenu_historyMenu toolbarbutton.subviewbutton",
200+
)
201+
202+
203+
PANEL_ITEMS = {
204+
"PanelUI-menu-button": PanelUI,
205+
"appMenu-history-button": History,
206+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
# You can obtain one at http://mozilla.org/MPL/2.0/.
4+
"""Creates Navbar object to interact with Firefox URL Bar."""
5+
6+
from selenium.webdriver.common.by import By
7+
from selenium.webdriver.remote.webdriver import WebDriver
8+
from selenium.webdriver.support.wait import WebDriverWait
9+
from foxpuppet.region import Region
10+
11+
12+
class UrlBar(Region):
13+
def suggestions(self, url: str) -> list[str]:
14+
"""
15+
Get all URL suggestions shown in the URL bar.
16+
17+
Args:
18+
url (str): The URL to type into the URL bar
19+
20+
Returns:
21+
list[str]: List of suggested URLs that appear in the URL bar
22+
"""
23+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
24+
url_bar = self.selenium.find_element(*URLBarLocators.INPUT_FIELD)
25+
url_bar.clear()
26+
url_bar.send_keys(url)
27+
28+
self.wait.until(
29+
lambda _: self.selenium.find_elements(*URLBarLocators.SEARCH_RESULTS)
30+
)
31+
32+
search_results = self.selenium.find_elements(
33+
*URLBarLocators.SEARCH_RESULT_ITEMS
34+
)
35+
36+
suggested_urls = [
37+
result.find_element(*URLBarLocators.SEARCH_RESULT_ITEM).text
38+
for result in search_results
39+
if result.find_element(*URLBarLocators.SEARCH_RESULT_ITEM).text
40+
]
41+
42+
return suggested_urls
43+
44+
45+
class URLBarLocators:
46+
INPUT_FIELD = (By.ID, "urlbar-input")
47+
SEARCH_RESULTS = (By.ID, "urlbar-results")
48+
SEARCH_RESULT_ITEM = (By.CSS_SELECTOR, "span.urlbarView-url")
49+
SEARCH_RESULT_ITEMS = (By.CSS_SELECTOR, "div.urlbarView-row[role='presentation']")

foxpuppet/windows/browser/window.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,32 @@
1111
from foxpuppet.windows.browser.navbar import NavBar
1212
from foxpuppet.windows.browser.notifications import BaseNotification
1313
from foxpuppet.windows.browser.bookmarks.bookmark import Bookmark
14+
from foxpuppet.windows.browser.panel_ui.panel_ui import PanelUI
1415
from selenium.webdriver.remote.webelement import WebElement
1516
from typing import Any, Optional, Union, TypeVar, Type
1617

1718
T = TypeVar("T", bound="BaseNotification")
19+
P = TypeVar("P", bound="PanelUI")
1820

1921

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

23-
_bookmark_locator = (By.ID, "main-window") # editBookmarkPanelTemplate
25+
_bookmark_locator = (By.ID, "main-window")
2426
_file_menu_button_locator = (By.ID, "file-menu")
2527
_file_menu_private_window_locator = (By.ID, "menu_newPrivateWindow")
2628
_file_menu_new_window_button_locator = (By.ID, "menu_newNavigator")
2729
_nav_bar_locator = (By.ID, "nav-bar")
2830
_notification_locator = (By.CSS_SELECTOR, "#notification-popup popupnotification")
31+
_panel_ui_locator = (By.ID, "PanelUI-menu-button")
2932
_app_menu_notification_locator = (
3033
By.CSS_SELECTOR,
3134
"#appMenu-notification-popup popupnotification",
3235
)
36+
_app_menu_panel_ui_locator = (
37+
By.CSS_SELECTOR,
38+
"#appMenu-mainView .panel-subview-body toolbarbutton",
39+
)
3340
_tab_browser_locator = (By.ID, "tabbrowser-tabs")
3441

3542
@property
@@ -81,6 +88,23 @@ def bookmark(self) -> Bookmark:
8188
root = self.selenium.find_element(*self._bookmark_locator)
8289
return Bookmark.create(self, root)
8390

91+
@property
92+
def panel(self) -> PanelUI | Any:
93+
panel_root = None
94+
with self.selenium.context(self.selenium.CONTEXT_CHROME):
95+
root = self.selenium.find_element(*self._panel_ui_locator)
96+
panel_root = PanelUI.create(self, root)
97+
98+
panel_items = self.selenium.find_elements(*self._app_menu_panel_ui_locator)
99+
for item in panel_items:
100+
_id = item.get_property("id")
101+
from foxpuppet.windows.browser.panel_ui.panel_ui import PANEL_ITEMS
102+
103+
if _id in PANEL_ITEMS and item.is_displayed():
104+
panel_root = PANEL_ITEMS[_id].create(self, item) # type: ignore
105+
106+
return panel_root
107+
84108
def wait_for_notification(
85109
self,
86110
notification_class: Optional[Type[T]] = BaseNotification, # type: ignore
@@ -129,6 +153,36 @@ def wait_for_bookmark(self) -> Bookmark:
129153
)
130154
return self.bookmark
131155

156+
def wait_for_panel(
157+
self, panel_ui_class: Optional[Type[P]] = PanelUI # type: ignore
158+
) -> Optional[P]:
159+
"""Wait for the specified PanelUI item to be displayed.
160+
161+
Args:
162+
panel_ui_class (:py:class:`PanelUI`, optional):
163+
The PanelUI subclass to wait for. If `None` is specified, it
164+
will wait for any panel UI to be displayed. Defaults to `PanelUI`.
165+
166+
Returns:
167+
Optional[:py:class:`PanelUI`]: The displayed PanelUI or `None` if not found.
168+
"""
169+
if panel_ui_class:
170+
if panel_ui_class is PanelUI:
171+
message = "No panel UI was shown."
172+
else:
173+
message = f"{panel_ui_class.__name__} was not shown."
174+
self.wait.until(
175+
lambda _: isinstance(self.panel, panel_ui_class),
176+
message=message,
177+
)
178+
return self.panel # type: ignore
179+
else:
180+
self.wait.until(
181+
lambda _: self.panel is None,
182+
message="Unexpected panel UI was shown.",
183+
)
184+
return None
185+
132186
@property
133187
def is_private(self) -> bool | Any:
134188
"""Property that checks if the specified window is private or not.

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
addopts = -vvv --driver Firefox --cov --cov-fail-under=95 --html=results/report.html --self-contained-html
3+
testpaths = tests

0 commit comments

Comments
 (0)