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.
Files changed (102) hide show
  1. auto_fxui/__init__.py +3 -0
  2. auto_fxui/browser_object.py +20 -0
  3. auto_fxui/browser_object_autofill_popup.py +275 -0
  4. auto_fxui/browser_object_context_menu.py +261 -0
  5. auto_fxui/browser_object_devtools.py +11 -0
  6. auto_fxui/browser_object_find_toolbar.py +120 -0
  7. auto_fxui/browser_object_forget_panel.py +37 -0
  8. auto_fxui/browser_object_glean.py +165 -0
  9. auto_fxui/browser_object_menu_bar.py +49 -0
  10. auto_fxui/browser_object_navigation.py +1860 -0
  11. auto_fxui/browser_object_panel_ui.py +530 -0
  12. auto_fxui/browser_object_print_preview.py +249 -0
  13. auto_fxui/browser_object_reader_view.py +130 -0
  14. auto_fxui/browser_object_report_broken_site.py +80 -0
  15. auto_fxui/browser_object_sidebar.py +519 -0
  16. auto_fxui/browser_object_smart_bar.py +486 -0
  17. auto_fxui/browser_object_smart_window.py +365 -0
  18. auto_fxui/browser_object_smart_window_chat.py +265 -0
  19. auto_fxui/browser_object_split_view.py +214 -0
  20. auto_fxui/browser_object_tabbar.py +789 -0
  21. auto_fxui/browser_object_translations_panel.py +114 -0
  22. auto_fxui/browser_object_trust_panel.py +404 -0
  23. auto_fxui/classes/autofill_base.py +47 -0
  24. auto_fxui/classes/bookmark.py +23 -0
  25. auto_fxui/classes/credit_card.py +38 -0
  26. auto_fxui/classes/fxa_session.py +20 -0
  27. auto_fxui/classes/recording.py +270 -0
  28. auto_fxui/classes/tab_grouping_scenario.py +56 -0
  29. auto_fxui/components/dropdown.py +109 -0
  30. auto_fxui/data/about_addons.components.json +103 -0
  31. auto_fxui/data/about_cache.components.json +38 -0
  32. auto_fxui/data/about_config.components.json +80 -0
  33. auto_fxui/data/about_downloads.components.json +20 -0
  34. auto_fxui/data/about_downloads_context_menu.components.json +104 -0
  35. auto_fxui/data/about_glean.components.json +42 -0
  36. auto_fxui/data/about_logins.components.json +305 -0
  37. auto_fxui/data/about_networking.components.json +20 -0
  38. auto_fxui/data/about_newtab.components.json +179 -0
  39. auto_fxui/data/about_opentabs.components.json +73 -0
  40. auto_fxui/data/about_prefs.components.json +2644 -0
  41. auto_fxui/data/about_privatebrowsing.components.json +25 -0
  42. auto_fxui/data/about_profiles.components.json +64 -0
  43. auto_fxui/data/about_protections.components.json +49 -0
  44. auto_fxui/data/about_telemetry.components.json +83 -0
  45. auto_fxui/data/about_translations.components.json +64 -0
  46. auto_fxui/data/address_fill.components.json +107 -0
  47. auto_fxui/data/amo_languages.components.json +120 -0
  48. auto_fxui/data/amo_themes.components.json +29 -0
  49. auto_fxui/data/autofill_popup.components.json +299 -0
  50. auto_fxui/data/context_menu.components.json +413 -0
  51. auto_fxui/data/credit_card_fill.components.json +26 -0
  52. auto_fxui/data/customize_firefox.components.json +80 -0
  53. auto_fxui/data/devtools.components.json +23 -0
  54. auto_fxui/data/error_page.components.json +61 -0
  55. auto_fxui/data/example_page.components.json +31 -0
  56. auto_fxui/data/find_toolbar.components.json +84 -0
  57. auto_fxui/data/firefox_view.components.json +3 -0
  58. auto_fxui/data/forget_panel.components.json +39 -0
  59. auto_fxui/data/fxa_home.components.json +182 -0
  60. auto_fxui/data/generic_page.components.json +437 -0
  61. auto_fxui/data/generic_pdf.components.json +635 -0
  62. auto_fxui/data/glean.components.json +3 -0
  63. auto_fxui/data/google_search.components.json +13 -0
  64. auto_fxui/data/google_sheets.components.json +43 -0
  65. auto_fxui/data/login_autofill.components.json +170 -0
  66. auto_fxui/data/menu_bar.components.json +93 -0
  67. auto_fxui/data/navigation.components.json +1497 -0
  68. auto_fxui/data/panel_ui.components.json +588 -0
  69. auto_fxui/data/print_preview.components.json +56 -0
  70. auto_fxui/data/reader_view.components.json +264 -0
  71. auto_fxui/data/report_broken_site.components.json +84 -0
  72. auto_fxui/data/sidebar.components.json +56 -0
  73. auto_fxui/data/smart_bar.components.json +13 -0
  74. auto_fxui/data/smart_window.components.json +76 -0
  75. auto_fxui/data/smart_window_chat.components.json +3 -0
  76. auto_fxui/data/smart_window_first_run.components.json +128 -0
  77. auto_fxui/data/split_view.components.json +85 -0
  78. auto_fxui/data/tab_bar.components.json +384 -0
  79. auto_fxui/data/text_area_form_autofill.components.json +11 -0
  80. auto_fxui/data/translations_panel.components.json +75 -0
  81. auto_fxui/data/trust_panel.components.json +428 -0
  82. auto_fxui/mock_server.py +275 -0
  83. auto_fxui/page_base.py +1356 -0
  84. auto_fxui/page_object.py +12 -0
  85. auto_fxui/page_object_about_pages.py +942 -0
  86. auto_fxui/page_object_addons_mozilla_org.py +94 -0
  87. auto_fxui/page_object_autofill.py +836 -0
  88. auto_fxui/page_object_customize_firefox.py +49 -0
  89. auto_fxui/page_object_error_page.py +55 -0
  90. auto_fxui/page_object_example_page.py +19 -0
  91. auto_fxui/page_object_firefox_view.py +69 -0
  92. auto_fxui/page_object_fxa_home.py +169 -0
  93. auto_fxui/page_object_generics.py +953 -0
  94. auto_fxui/page_object_google.py +86 -0
  95. auto_fxui/page_object_newtab.py +165 -0
  96. auto_fxui/page_object_prefs.py +2373 -0
  97. auto_fxui/page_object_smart_window_first_run.py +115 -0
  98. auto_fxui/util.py +998 -0
  99. auto_fxui-0.1.1.dist-info/METADATA +34 -0
  100. auto_fxui-0.1.1.dist-info/RECORD +102 -0
  101. auto_fxui-0.1.1.dist-info/WHEEL +4 -0
  102. auto_fxui-0.1.1.dist-info/licenses/LICENSE +373 -0
auto_fxui/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .page_base import BasePage
2
+
3
+ __all__ = ["BasePage"]
@@ -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
@@ -0,0 +1,11 @@
1
+ from .page_base import BasePage
2
+
3
+
4
+ class Devtools(BasePage):
5
+ """BOM for the DevTools panel"""
6
+
7
+ URL_TEMPLATE = ""
8
+
9
+ def check_opened(self) -> BasePage:
10
+ self.wait_for_page_to_load()
11
+ return self