auto-fxui 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_fxui/__init__.py +3 -0
- auto_fxui/browser_object.py +20 -0
- auto_fxui/browser_object_autofill_popup.py +275 -0
- auto_fxui/browser_object_context_menu.py +261 -0
- auto_fxui/browser_object_devtools.py +11 -0
- auto_fxui/browser_object_find_toolbar.py +120 -0
- auto_fxui/browser_object_forget_panel.py +37 -0
- auto_fxui/browser_object_glean.py +165 -0
- auto_fxui/browser_object_menu_bar.py +49 -0
- auto_fxui/browser_object_navigation.py +1860 -0
- auto_fxui/browser_object_panel_ui.py +530 -0
- auto_fxui/browser_object_print_preview.py +249 -0
- auto_fxui/browser_object_reader_view.py +130 -0
- auto_fxui/browser_object_report_broken_site.py +80 -0
- auto_fxui/browser_object_sidebar.py +519 -0
- auto_fxui/browser_object_smart_bar.py +486 -0
- auto_fxui/browser_object_smart_window.py +365 -0
- auto_fxui/browser_object_smart_window_chat.py +265 -0
- auto_fxui/browser_object_split_view.py +214 -0
- auto_fxui/browser_object_tabbar.py +789 -0
- auto_fxui/browser_object_translations_panel.py +114 -0
- auto_fxui/browser_object_trust_panel.py +404 -0
- auto_fxui/classes/autofill_base.py +47 -0
- auto_fxui/classes/bookmark.py +23 -0
- auto_fxui/classes/credit_card.py +38 -0
- auto_fxui/classes/fxa_session.py +20 -0
- auto_fxui/classes/recording.py +270 -0
- auto_fxui/classes/tab_grouping_scenario.py +56 -0
- auto_fxui/components/dropdown.py +109 -0
- auto_fxui/data/about_addons.components.json +103 -0
- auto_fxui/data/about_cache.components.json +38 -0
- auto_fxui/data/about_config.components.json +80 -0
- auto_fxui/data/about_downloads.components.json +20 -0
- auto_fxui/data/about_downloads_context_menu.components.json +104 -0
- auto_fxui/data/about_glean.components.json +42 -0
- auto_fxui/data/about_logins.components.json +305 -0
- auto_fxui/data/about_networking.components.json +20 -0
- auto_fxui/data/about_newtab.components.json +179 -0
- auto_fxui/data/about_opentabs.components.json +73 -0
- auto_fxui/data/about_prefs.components.json +2644 -0
- auto_fxui/data/about_privatebrowsing.components.json +25 -0
- auto_fxui/data/about_profiles.components.json +64 -0
- auto_fxui/data/about_protections.components.json +49 -0
- auto_fxui/data/about_telemetry.components.json +83 -0
- auto_fxui/data/about_translations.components.json +64 -0
- auto_fxui/data/address_fill.components.json +107 -0
- auto_fxui/data/amo_languages.components.json +120 -0
- auto_fxui/data/amo_themes.components.json +29 -0
- auto_fxui/data/autofill_popup.components.json +299 -0
- auto_fxui/data/context_menu.components.json +413 -0
- auto_fxui/data/credit_card_fill.components.json +26 -0
- auto_fxui/data/customize_firefox.components.json +80 -0
- auto_fxui/data/devtools.components.json +23 -0
- auto_fxui/data/error_page.components.json +61 -0
- auto_fxui/data/example_page.components.json +31 -0
- auto_fxui/data/find_toolbar.components.json +84 -0
- auto_fxui/data/firefox_view.components.json +3 -0
- auto_fxui/data/forget_panel.components.json +39 -0
- auto_fxui/data/fxa_home.components.json +182 -0
- auto_fxui/data/generic_page.components.json +437 -0
- auto_fxui/data/generic_pdf.components.json +635 -0
- auto_fxui/data/glean.components.json +3 -0
- auto_fxui/data/google_search.components.json +13 -0
- auto_fxui/data/google_sheets.components.json +43 -0
- auto_fxui/data/login_autofill.components.json +170 -0
- auto_fxui/data/menu_bar.components.json +93 -0
- auto_fxui/data/navigation.components.json +1497 -0
- auto_fxui/data/panel_ui.components.json +588 -0
- auto_fxui/data/print_preview.components.json +56 -0
- auto_fxui/data/reader_view.components.json +264 -0
- auto_fxui/data/report_broken_site.components.json +84 -0
- auto_fxui/data/sidebar.components.json +56 -0
- auto_fxui/data/smart_bar.components.json +13 -0
- auto_fxui/data/smart_window.components.json +76 -0
- auto_fxui/data/smart_window_chat.components.json +3 -0
- auto_fxui/data/smart_window_first_run.components.json +128 -0
- auto_fxui/data/split_view.components.json +85 -0
- auto_fxui/data/tab_bar.components.json +384 -0
- auto_fxui/data/text_area_form_autofill.components.json +11 -0
- auto_fxui/data/translations_panel.components.json +75 -0
- auto_fxui/data/trust_panel.components.json +428 -0
- auto_fxui/mock_server.py +275 -0
- auto_fxui/page_base.py +1356 -0
- auto_fxui/page_object.py +12 -0
- auto_fxui/page_object_about_pages.py +942 -0
- auto_fxui/page_object_addons_mozilla_org.py +94 -0
- auto_fxui/page_object_autofill.py +836 -0
- auto_fxui/page_object_customize_firefox.py +49 -0
- auto_fxui/page_object_error_page.py +55 -0
- auto_fxui/page_object_example_page.py +19 -0
- auto_fxui/page_object_firefox_view.py +69 -0
- auto_fxui/page_object_fxa_home.py +169 -0
- auto_fxui/page_object_generics.py +953 -0
- auto_fxui/page_object_google.py +86 -0
- auto_fxui/page_object_newtab.py +165 -0
- auto_fxui/page_object_prefs.py +2373 -0
- auto_fxui/page_object_smart_window_first_run.py +115 -0
- auto_fxui/util.py +998 -0
- auto_fxui-0.1.1.dist-info/METADATA +34 -0
- auto_fxui-0.1.1.dist-info/RECORD +102 -0
- auto_fxui-0.1.1.dist-info/WHEEL +4 -0
- auto_fxui-0.1.1.dist-info/licenses/LICENSE +373 -0
auto_fxui/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .browser_object_autofill_popup import *
|
|
2
|
+
from .browser_object_context_menu import *
|
|
3
|
+
from .browser_object_devtools import *
|
|
4
|
+
from .browser_object_find_toolbar import *
|
|
5
|
+
from .browser_object_forget_panel import *
|
|
6
|
+
from .browser_object_glean import *
|
|
7
|
+
from .browser_object_menu_bar import *
|
|
8
|
+
from .browser_object_navigation import *
|
|
9
|
+
from .browser_object_panel_ui import *
|
|
10
|
+
from .browser_object_print_preview import *
|
|
11
|
+
from .browser_object_reader_view import *
|
|
12
|
+
from .browser_object_report_broken_site import *
|
|
13
|
+
from .browser_object_sidebar import *
|
|
14
|
+
from .browser_object_smart_bar import *
|
|
15
|
+
from .browser_object_smart_window import *
|
|
16
|
+
from .browser_object_smart_window_chat import *
|
|
17
|
+
from .browser_object_split_view import *
|
|
18
|
+
from .browser_object_tabbar import *
|
|
19
|
+
from .browser_object_translations_panel import *
|
|
20
|
+
from .browser_object_trust_panel import *
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from selenium.common.exceptions import StaleElementReferenceException
|
|
5
|
+
from selenium.webdriver import Keys
|
|
6
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
7
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
8
|
+
|
|
9
|
+
from .page_base import BasePage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AutofillPopup(BasePage):
|
|
13
|
+
"""
|
|
14
|
+
Page Object Model for the popups that have to do with autofill (related to credit card and addresses)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
URL_TEMPLATE = ""
|
|
18
|
+
iframe = None
|
|
19
|
+
|
|
20
|
+
# Verification methods
|
|
21
|
+
def verify_autofill_displayed(self):
|
|
22
|
+
"""Confirms that autofill popup has loaded"""
|
|
23
|
+
self.element_clickable("autofill-panel")
|
|
24
|
+
|
|
25
|
+
def verify_element_displayed(self, reference: Union[str, tuple, WebElement]):
|
|
26
|
+
"""Confirms that an element exists in popup"""
|
|
27
|
+
self.element_clickable(reference)
|
|
28
|
+
|
|
29
|
+
@BasePage.context_chrome
|
|
30
|
+
def ensure_autofill_dropdown_not_visible(self):
|
|
31
|
+
"""
|
|
32
|
+
Verifies that the autofill dropdown does not appear
|
|
33
|
+
checks if the parent pop up component has hidden attribute.
|
|
34
|
+
"""
|
|
35
|
+
self.element_exists("pop-up-component")
|
|
36
|
+
popup_component = self.get_element("pop-up-component")
|
|
37
|
+
if len(popup_component.get_attribute("innerHTML")) > 1:
|
|
38
|
+
self.element_attribute_contains("pop-up-component-box", "style", "0px;")
|
|
39
|
+
else:
|
|
40
|
+
self.element_attribute_contains("pop-up-component", "hidden", "true")
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
@BasePage.context_chrome
|
|
44
|
+
def ensure_autofill_dropdown_visible(self):
|
|
45
|
+
"""
|
|
46
|
+
Verifies that the autofill dropdown appears
|
|
47
|
+
checks if the parent pop up component has hidden attribute.
|
|
48
|
+
"""
|
|
49
|
+
self.element_clickable("pop-up-component-box")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
@BasePage.context_chrome
|
|
53
|
+
def hover_over_autofill_panel(self):
|
|
54
|
+
self.element_clickable("pop-up-component-box")
|
|
55
|
+
self.hover("pop-up-component-box")
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
# Interaction with popup elements
|
|
59
|
+
def click_doorhanger_button(self, button_type: str) -> BasePage:
|
|
60
|
+
"""Presses a button in the doorhanger popup (e.g., save, update, dropdown)"""
|
|
61
|
+
self.element_visible(f"doorhanger-{button_type}-button")
|
|
62
|
+
self.js_click_on(f"doorhanger-{button_type}-button")
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
@BasePage.context_chrome
|
|
66
|
+
def open_login_doorhanger_more_actions_menu(self) -> BasePage:
|
|
67
|
+
"""Open the login doorhanger's 'More actions' menu and wait for it to render."""
|
|
68
|
+
self.element_visible("doorhanger-secondary-split-button")
|
|
69
|
+
self.js_click_on("doorhanger-more-actions-chevron")
|
|
70
|
+
self.element_visible("doorhanger-never-save-login-button")
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
# Interaction with form options
|
|
74
|
+
@BasePage.context_chrome
|
|
75
|
+
def click_autofill_form_option(self) -> BasePage:
|
|
76
|
+
"""Clicks the credit card or address selection in the autofill panel"""
|
|
77
|
+
self.get_element("select-form-option").click()
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
@BasePage.context_chrome
|
|
81
|
+
def click_clear_form_option(self) -> BasePage:
|
|
82
|
+
"""Clicks to clear the saved form option (address or credit card).
|
|
83
|
+
|
|
84
|
+
Firefox 154+ renders the footer rows ("Clear Autofill Form",
|
|
85
|
+
"Manage payment methods") with the same ``originaltype='action'`` and no
|
|
86
|
+
``ac-value`` attribute, so the row is matched by its shadow-DOM label.
|
|
87
|
+
"""
|
|
88
|
+
option = self.wait.until(
|
|
89
|
+
lambda _: self._get_option_by_value("Clear Autofill Form")
|
|
90
|
+
)
|
|
91
|
+
option.click()
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def get_doorhanger_cc_number(self) -> str:
|
|
95
|
+
"""Retrieves the last 4 digits of the credit card from the doorhanger popup"""
|
|
96
|
+
return self.get_element("doorhanger-cc-number").text
|
|
97
|
+
|
|
98
|
+
@BasePage.context_chrome
|
|
99
|
+
def get_cc_doorhanger_data(self, selector: str) -> str:
|
|
100
|
+
"""
|
|
101
|
+
get text for the credit card doorhanger data.
|
|
102
|
+
"""
|
|
103
|
+
return self.get_element(selector).text
|
|
104
|
+
|
|
105
|
+
# Interaction with autocomplete list elements
|
|
106
|
+
@BasePage.context_chrome
|
|
107
|
+
def get_nth_element(self, index: str | int) -> WebElement:
|
|
108
|
+
"""
|
|
109
|
+
Get the nth element from the autocomplete list
|
|
110
|
+
Parameters: index (str): The index of the element to retrieve (1-based)
|
|
111
|
+
Returns: WebElement: The nth element in the autocomplete list
|
|
112
|
+
"""
|
|
113
|
+
self.wait.until(
|
|
114
|
+
EC.visibility_of(
|
|
115
|
+
self.get_element("select-form-option-by-index", labels=[str(index)])
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return self.get_element("select-form-option-by-index", labels=[str(index)])
|
|
119
|
+
|
|
120
|
+
@BasePage.context_chrome
|
|
121
|
+
def select_nth_element(self, index: int):
|
|
122
|
+
"""
|
|
123
|
+
Select the nth element from the autocomplete list
|
|
124
|
+
Arguments:
|
|
125
|
+
index (int): The index of the element to retrieve (1-based)
|
|
126
|
+
"""
|
|
127
|
+
self.element_clickable("pop-up-component-box")
|
|
128
|
+
self.click_on("select-form-option-by-index", labels=[str(index)])
|
|
129
|
+
|
|
130
|
+
@BasePage.context_chrome
|
|
131
|
+
def select_autofill_panel(self):
|
|
132
|
+
"""
|
|
133
|
+
Select the first autofill panel in the autocomplete list.
|
|
134
|
+
"""
|
|
135
|
+
self.element_clickable("select-form-option-autofill")
|
|
136
|
+
self.click_on("select-form-option-autofill")
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
@BasePage.context_chrome
|
|
140
|
+
def get_primary_value(self, element: WebElement) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Get the primary (label) value from an autocomplete row element.
|
|
143
|
+
|
|
144
|
+
Firefox 154 replaced the classic ``richlistitem[ac-value]`` rows with the
|
|
145
|
+
``autocomplete-row-item`` custom element, moving the visible text into its
|
|
146
|
+
shadow DOM (``.labels-container .label``) and dropping the ``ac-value``
|
|
147
|
+
attribute. Prefer ``ac-value`` when present (older builds) and otherwise
|
|
148
|
+
read the shadow label.
|
|
149
|
+
|
|
150
|
+
Parameters: element (WebElement): The autocomplete row element.
|
|
151
|
+
Returns: str: The primary value (e.g. username, cardholder name, or footer label).
|
|
152
|
+
"""
|
|
153
|
+
return self._get_primary_value(element)
|
|
154
|
+
|
|
155
|
+
def _get_primary_value(self, element: WebElement) -> str:
|
|
156
|
+
"""Chrome-context body of :meth:`get_primary_value`.
|
|
157
|
+
|
|
158
|
+
Intentionally undecorated so it can be called from code already inside a
|
|
159
|
+
chrome context (e.g. ``get_option_by_value`` iterating rows) without
|
|
160
|
+
re-entering ``driver.context`` on every access.
|
|
161
|
+
"""
|
|
162
|
+
ac_value = element.get_attribute("ac-value")
|
|
163
|
+
if ac_value:
|
|
164
|
+
return ac_value
|
|
165
|
+
return self.driver.execute_script(
|
|
166
|
+
"""
|
|
167
|
+
const row = arguments[0].querySelector('autocomplete-row-item');
|
|
168
|
+
if (!row || !row.shadowRoot) return null;
|
|
169
|
+
const label = row.shadowRoot.querySelector('.labels-container .label, .label');
|
|
170
|
+
return label ? label.textContent.trim() : null;
|
|
171
|
+
""",
|
|
172
|
+
element,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@BasePage.context_chrome
|
|
176
|
+
def get_option_by_value(self, value: str) -> WebElement | None:
|
|
177
|
+
"""
|
|
178
|
+
Return the first visible autocomplete row whose primary value contains
|
|
179
|
+
`value`, or None if no such row is present.
|
|
180
|
+
"""
|
|
181
|
+
return self._get_option_by_value(value)
|
|
182
|
+
|
|
183
|
+
def _get_option_by_value(self, value: str) -> WebElement | None:
|
|
184
|
+
"""Chrome-context body of :meth:`get_option_by_value`.
|
|
185
|
+
|
|
186
|
+
Undecorated so it can be polled inside a ``wait.until`` from a method that
|
|
187
|
+
already holds a chrome context (see :meth:`click_clear_form_option`,
|
|
188
|
+
:meth:`verify_autocomplete_option`) without a nested context switch on
|
|
189
|
+
every tick.
|
|
190
|
+
|
|
191
|
+
If the dropdown re-renders mid-iteration the rows go stale; return None
|
|
192
|
+
so a ``wait.until`` caller simply retries on the next tick (self.wait does
|
|
193
|
+
not ignore StaleElementReferenceException, so it must be caught here).
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
for option in self.get_elements("select-form-option"):
|
|
197
|
+
primary = self._get_primary_value(option)
|
|
198
|
+
if primary and value in primary and option.is_displayed():
|
|
199
|
+
return option
|
|
200
|
+
except StaleElementReferenceException:
|
|
201
|
+
return None
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
@BasePage.context_chrome
|
|
205
|
+
def verify_update_password_doorhanger(self, nav, expected_text):
|
|
206
|
+
"""
|
|
207
|
+
Wait for and verify that the 'Update password' doorhanger is displayed
|
|
208
|
+
with the expected text
|
|
209
|
+
"""
|
|
210
|
+
# Wait for and open the doorhanger
|
|
211
|
+
time.sleep(1)
|
|
212
|
+
nav.expect(lambda _: nav.element_visible("password-notification-key"))
|
|
213
|
+
nav.click_on("password-notification-key")
|
|
214
|
+
|
|
215
|
+
# Verify the doorhanger text
|
|
216
|
+
self.expect(
|
|
217
|
+
lambda _: (
|
|
218
|
+
expected_text in self.get_element("password-update-doorhanger").text
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@BasePage.context_chrome
|
|
223
|
+
def click_manage_passwords(self) -> BasePage:
|
|
224
|
+
# Clicks the "Manage Passwords" option from the autofill popup
|
|
225
|
+
self.click_on("manage-passwords")
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
@BasePage.context_chrome
|
|
229
|
+
def verify_username_value(
|
|
230
|
+
self,
|
|
231
|
+
expected_username: str,
|
|
232
|
+
field: str = "password-notification-username-field",
|
|
233
|
+
) -> WebElement:
|
|
234
|
+
"""Wait until the username field contains the expected value."""
|
|
235
|
+
element = self.get_element(field)
|
|
236
|
+
self.wait.until(lambda _: element.get_attribute("value") == expected_username)
|
|
237
|
+
return element
|
|
238
|
+
|
|
239
|
+
@BasePage.context_chrome
|
|
240
|
+
def click_securely_generated_password(self) -> BasePage:
|
|
241
|
+
"""Click the 'Use a Securely Generated Password' option from the autofill popup."""
|
|
242
|
+
self.click_on("generated-securely-password")
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
@BasePage.context_chrome
|
|
246
|
+
def verify_autocomplete_option(self, value: str) -> BasePage:
|
|
247
|
+
"""Wait until an autocomplete option whose label contains `value` is displayed."""
|
|
248
|
+
self.wait.until(lambda _: self._get_option_by_value(value) is not None)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def _get_doorhanger_username_input(self):
|
|
252
|
+
"""Helper to get the input element inside the password doorhanger username shadow DOM.
|
|
253
|
+
Must be called from chrome context.
|
|
254
|
+
"""
|
|
255
|
+
parent = self.get_element("password-notification-username-field")
|
|
256
|
+
input_el = self.driver.execute_script(
|
|
257
|
+
"return arguments[0].shadowRoot.querySelector('input')", parent
|
|
258
|
+
)
|
|
259
|
+
if input_el is None:
|
|
260
|
+
raise RuntimeError(
|
|
261
|
+
"Could not find input inside password-notification-username-field shadow root"
|
|
262
|
+
)
|
|
263
|
+
return input_el
|
|
264
|
+
|
|
265
|
+
@BasePage.context_chrome
|
|
266
|
+
def type_username_in_password_doorhanger(self, username: str) -> BasePage:
|
|
267
|
+
"""Type a username into the Password Manager doorhanger."""
|
|
268
|
+
self._get_doorhanger_username_input().send_keys(username)
|
|
269
|
+
return self
|
|
270
|
+
|
|
271
|
+
@BasePage.context_chrome
|
|
272
|
+
def dismiss_password_doorhanger(self) -> BasePage:
|
|
273
|
+
"""Dismiss the Password Manager doorhanger using ESC."""
|
|
274
|
+
self._get_doorhanger_username_input().send_keys(Keys.ESCAPE)
|
|
275
|
+
return self
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, List, Union
|
|
3
|
+
|
|
4
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
5
|
+
|
|
6
|
+
from .page_base import BasePage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextMenu(BasePage):
|
|
10
|
+
"""
|
|
11
|
+
Browser Object Model (base class/parent class) for the context menus upon right clicking
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
URL_TEMPLATE = ""
|
|
15
|
+
|
|
16
|
+
@BasePage.context_chrome
|
|
17
|
+
def click_context_item(
|
|
18
|
+
self, reference: Union[str, tuple, WebElement], labels=[]
|
|
19
|
+
) -> BasePage:
|
|
20
|
+
"""
|
|
21
|
+
Clicks the context item.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
self.fetch(reference, labels=labels).click()
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
@BasePage.context_chrome
|
|
28
|
+
def open_link_in_container(self) -> BasePage:
|
|
29
|
+
"""Open a link from the context menu in a specific container tab."""
|
|
30
|
+
self.click_context_item("context-menu-open-link-in-new_container_tab")
|
|
31
|
+
self.click_context_item("context-menu-open-link-in-container-work")
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
@BasePage.context_chrome
|
|
35
|
+
def verify_topsites_tile_context_menu_options(
|
|
36
|
+
self,
|
|
37
|
+
static_items: Dict[str, str],
|
|
38
|
+
dynamic_items: List[str],
|
|
39
|
+
tile_title: str,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Verifies expected context menu options are present upon right clicking a topsite tile.
|
|
43
|
+
Arguments:
|
|
44
|
+
static_items: Dict mapping of selector name to expected label text.
|
|
45
|
+
dynamic_items: List of selector names for items with dynamic labels.
|
|
46
|
+
tile_title: Optional, required if dynamic label validation is needed (e.g., Wikipedia, YouTube).
|
|
47
|
+
"""
|
|
48
|
+
# --- Static items ---
|
|
49
|
+
for selector, expected_label in static_items.items():
|
|
50
|
+
option = self.get_element(selector)
|
|
51
|
+
label = (option.get_attribute("label") or option.text or "").strip()
|
|
52
|
+
assert expected_label in label, (
|
|
53
|
+
f'Expected label "{expected_label}" not found. Got: "{label}"'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# --- Dynamic items ---
|
|
57
|
+
for selector in dynamic_items:
|
|
58
|
+
option = self.get_element(selector)
|
|
59
|
+
label = (option.get_attribute("label") or option.text or "").strip()
|
|
60
|
+
normalized = label.lower()
|
|
61
|
+
assert normalized.startswith("search"), (
|
|
62
|
+
f'Label does not start with "Search": "{label}"'
|
|
63
|
+
)
|
|
64
|
+
assert "for" in normalized, f'"for" not found in label: "{label}"'
|
|
65
|
+
assert tile_title.lower() in normalized, (
|
|
66
|
+
f'Search term "{tile_title}" not found in label: "{label}"'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@BasePage.context_chrome
|
|
70
|
+
def click_summarize_page_from_ai_chat(self):
|
|
71
|
+
"""Activate the Summarize Page item from the already opened AI Chat submenu.
|
|
72
|
+
|
|
73
|
+
Pure Selenium cannot interact with XUL submenu items in the chrome context: ActionChains
|
|
74
|
+
move_to_element does not cross GeckoDriver's chrome context boundary, and Selenium's click()
|
|
75
|
+
on a XUL <menu> element does not open its popup. JS is used to dispatch the full mouse event
|
|
76
|
+
sequence (mousemove/mousedown/mouseup/click) directly on the menuitem, which fires the XUL
|
|
77
|
+
command event that triggers the sidebar to open.
|
|
78
|
+
|
|
79
|
+
Note: the menuitem is selected by label="Summarize Page" which is English-only; no stable
|
|
80
|
+
ID or anonid is exposed by this XUL element.
|
|
81
|
+
"""
|
|
82
|
+
self.wait.until(
|
|
83
|
+
lambda _: self.driver.execute_script(
|
|
84
|
+
"const menu = document.getElementById('context_askChat');"
|
|
85
|
+
"if (!menu) return false;"
|
|
86
|
+
"const popup = menu.querySelector('menupopup') || menu.menupopup;"
|
|
87
|
+
"if (!popup) return false;"
|
|
88
|
+
"return popup.querySelector('menuitem[label=\"Summarize Page\"]') !== null;"
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
self.driver.execute_script(
|
|
92
|
+
"const menu = document.getElementById('context_askChat');"
|
|
93
|
+
"const popup = menu.querySelector('menupopup') || menu.menupopup;"
|
|
94
|
+
"const item = popup.querySelector('menuitem[label=\"Summarize Page\"]');"
|
|
95
|
+
"item.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));"
|
|
96
|
+
"item.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));"
|
|
97
|
+
"item.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));"
|
|
98
|
+
"item.dispatchEvent(new MouseEvent('click', {bubbles: true}));"
|
|
99
|
+
)
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
@BasePage.context_chrome
|
|
103
|
+
def click_choose_ai_chatbot_from_context_menu(
|
|
104
|
+
self, source: str = "tab"
|
|
105
|
+
) -> BasePage:
|
|
106
|
+
"""Click the 'Choose an AI Chatbot' item from the AI Chat submenu.
|
|
107
|
+
|
|
108
|
+
source='tab' targets the tab context menu (DOM id 'context_askChat');
|
|
109
|
+
source='page' targets the page context menu (DOM id 'context-ask-chat').
|
|
110
|
+
menu.open = true keeps the submenu alive while searching for its children.
|
|
111
|
+
"""
|
|
112
|
+
menu_id = "context_askChat" if source == "tab" else "context-ask-chat"
|
|
113
|
+
self.wait.until(
|
|
114
|
+
lambda _: self.driver.execute_script(
|
|
115
|
+
f"const menu = document.getElementById('{menu_id}');"
|
|
116
|
+
"if (!menu) return false;"
|
|
117
|
+
"menu.open = true;"
|
|
118
|
+
"const popup = menu.querySelector('menupopup') || menu.menupopup;"
|
|
119
|
+
"if (!popup || popup.children.length === 0) return false;"
|
|
120
|
+
"const item = popup.querySelector('[data-l10n-id=\"genai-menu-choose-chatbot\"]');"
|
|
121
|
+
"if (!item) return false;"
|
|
122
|
+
"item.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));"
|
|
123
|
+
"item.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));"
|
|
124
|
+
"item.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));"
|
|
125
|
+
"item.dispatchEvent(new MouseEvent('click', {bubbles: true}));"
|
|
126
|
+
"return true;"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
@BasePage.context_chrome
|
|
132
|
+
def click_open_chatbot_from_context_menu(self) -> BasePage:
|
|
133
|
+
"""Click the item that opens the AI chatbot panel from the tab context menu.
|
|
134
|
+
|
|
135
|
+
The tabContextMenu must be open before calling this (via tabs.context_click). Setting
|
|
136
|
+
menu.open = true on the XUL <menu> element opens the submenu inline while the parent
|
|
137
|
+
tabContextMenu is still open, which triggers provider-specific children to be generated
|
|
138
|
+
in the menupopup DOM. Pure Selenium cannot hover XUL elements across GeckoDriver's chrome
|
|
139
|
+
context boundary; ActionChains.move_to_element does not cross that boundary.
|
|
140
|
+
|
|
141
|
+
menu.open = true must remain inside the retry loop: the XUL submenu closes between
|
|
142
|
+
separate execute_script calls, so the standard check-then-act split cannot be used here.
|
|
143
|
+
|
|
144
|
+
The l10n-id for the open-chatbot item varies across Firefox builds; flexible matching on
|
|
145
|
+
l10n-id and label is intentional — targeting a hardcoded id caused timeouts in the
|
|
146
|
+
current build where that exact id is absent.
|
|
147
|
+
"""
|
|
148
|
+
self.wait.until(
|
|
149
|
+
lambda _: self.driver.execute_script(
|
|
150
|
+
"const menu = document.getElementById('context_askChat');"
|
|
151
|
+
"if (!menu) return false;"
|
|
152
|
+
"menu.open = true;"
|
|
153
|
+
"const popup = menu.querySelector('menupopup') || menu.menupopup;"
|
|
154
|
+
"if (!popup || popup.children.length === 0) return false;"
|
|
155
|
+
"const items = Array.from(popup.children).filter("
|
|
156
|
+
" item => !item.hidden && !item.disabled"
|
|
157
|
+
");"
|
|
158
|
+
"const item = items.find(item => {"
|
|
159
|
+
" const l10n = item.getAttribute('data-l10n-id') || '';"
|
|
160
|
+
" const label = (item.getAttribute('label') || '').toLowerCase();"
|
|
161
|
+
" return l10n.includes('open') || l10n.includes('provider') ||"
|
|
162
|
+
" label.includes('open') || label.includes('chat');"
|
|
163
|
+
"});"
|
|
164
|
+
"if (!item) return false;"
|
|
165
|
+
"item.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));"
|
|
166
|
+
"item.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));"
|
|
167
|
+
"item.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));"
|
|
168
|
+
"item.dispatchEvent(new MouseEvent('click', {bubbles: true}));"
|
|
169
|
+
"return true;"
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
@BasePage.context_chrome
|
|
175
|
+
def click_summarize_page_from_sidebar_ai_chat_button(self) -> BasePage:
|
|
176
|
+
"""Click 'Summarize Page' from the context menu opened by right-clicking the AI Chat
|
|
177
|
+
sidebar button.
|
|
178
|
+
|
|
179
|
+
The popup is at chrome-document level; a flat querySelectorAll('menupopup') is sufficient.
|
|
180
|
+
"""
|
|
181
|
+
self.wait.until(
|
|
182
|
+
lambda _: self.driver.execute_script(
|
|
183
|
+
"for (const popup of document.querySelectorAll('menupopup')) {"
|
|
184
|
+
" const s = popup.state;"
|
|
185
|
+
" if (s === 'closed' || s === 'hiding') continue;"
|
|
186
|
+
" const item = Array.from(popup.querySelectorAll('menuitem')).find(el =>"
|
|
187
|
+
" el.getAttribute('label') === 'Summarize Page'"
|
|
188
|
+
" );"
|
|
189
|
+
" if (!item) continue;"
|
|
190
|
+
" item.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));"
|
|
191
|
+
" item.dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));"
|
|
192
|
+
" item.dispatchEvent(new MouseEvent('mouseup', {bubbles: true}));"
|
|
193
|
+
" item.dispatchEvent(new MouseEvent('click', {bubbles: true}));"
|
|
194
|
+
" return true;"
|
|
195
|
+
"}"
|
|
196
|
+
"return false;"
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
@BasePage.context_chrome
|
|
202
|
+
def bookmark_tab_via_context_menu(self) -> BasePage:
|
|
203
|
+
"""Click 'Bookmark Tab' in the tab context menu, dismiss the menu, then
|
|
204
|
+
confirm the Add Bookmark dialog by clicking its Save button via JS.
|
|
205
|
+
|
|
206
|
+
The dialog renders inside browser.dialogFrame > #document >
|
|
207
|
+
dialog#bookmarkpropertiesdialog (shadow root), so JS is used to pierce
|
|
208
|
+
through contentDocument and shadow DOM.
|
|
209
|
+
"""
|
|
210
|
+
self.click_context_item("context-menu-bookmark-tab")
|
|
211
|
+
self.hide_popup("tabContextMenu")
|
|
212
|
+
self.wait.until(
|
|
213
|
+
lambda _: self.driver.execute_script(
|
|
214
|
+
"const cd = document.querySelector('browser.dialogFrame')?.contentDocument;"
|
|
215
|
+
"const btn = cd?.querySelector('dialog#bookmarkpropertiesdialog')"
|
|
216
|
+
"?.shadowRoot?.querySelector('button[dlgtype=\"accept\"]');"
|
|
217
|
+
"if (btn) { btn.click(); return true; }"
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
@BasePage.context_chrome
|
|
223
|
+
def expect_ask_ai_chat_submenu_absent(self) -> BasePage:
|
|
224
|
+
"""Verify the Ask AI chatbot submenu is not visible in the open tab context menu.
|
|
225
|
+
|
|
226
|
+
Gates on tabContextMenu being open first to prevent a false positive: context_askChat
|
|
227
|
+
is always in the DOM with hidden=true when the chatbot is disabled, so checking
|
|
228
|
+
before the menu opens would succeed even if the right-click never worked.
|
|
229
|
+
"""
|
|
230
|
+
self.wait.until(
|
|
231
|
+
lambda _: self.driver.execute_script(
|
|
232
|
+
"const menu = document.getElementById('tabContextMenu');"
|
|
233
|
+
"if (!menu || menu.state !== 'open') return false;"
|
|
234
|
+
"const el = document.getElementById('context_askChat');"
|
|
235
|
+
"return !el || el.hidden;"
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
@BasePage.context_chrome
|
|
241
|
+
def verify_item_disabled(self, reference: str, labels=None):
|
|
242
|
+
"""Assert a context menu item is disabled (grayed out)."""
|
|
243
|
+
el = self.get_element(reference, labels=labels)
|
|
244
|
+
assert el.get_attribute("disabled") == "true", f"{reference} is not disabled"
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class AboutDownloadsContextMenu(ContextMenu):
|
|
249
|
+
"""
|
|
250
|
+
Browser object model for the context menu for right clicking a download in About:Downloads
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def has_all_options_available(self) -> ContextMenu:
|
|
254
|
+
"""Timeout unless all items labeled downloadOption are present, else return self"""
|
|
255
|
+
with self.driver.context(self.context_id):
|
|
256
|
+
for elname in [
|
|
257
|
+
k for k, v in self.elements.items() if "downloadOption" in v["groups"]
|
|
258
|
+
]:
|
|
259
|
+
logging.info(f"elname {elname}")
|
|
260
|
+
self.element_exists(elname)
|
|
261
|
+
return self
|