pyxecm 2.0.4__py3-none-any.whl → 3.0.0__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.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/coreshare.py +5 -3
- pyxecm/helper/data.py +4 -4
- pyxecm/helper/otel_config.py +26 -0
- pyxecm/helper/web.py +1 -2
- pyxecm/otca.py +1356 -16
- pyxecm/otcs.py +2354 -593
- pyxecm/otds.py +1 -1
- pyxecm/otmm.py +4 -5
- pyxecm/py.typed +0 -0
- pyxecm-3.0.0.dist-info/METADATA +48 -0
- pyxecm-3.0.0.dist-info/RECORD +96 -0
- {pyxecm-2.0.4.dist-info → pyxecm-3.0.0.dist-info}/WHEEL +1 -2
- pyxecm-3.0.0.dist-info/entry_points.txt +4 -0
- {pyxecm/customizer/api → pyxecm_api}/__main__.py +1 -1
- pyxecm_api/agents/__init__.py +7 -0
- pyxecm_api/agents/app.py +13 -0
- pyxecm_api/agents/functions.py +119 -0
- pyxecm_api/agents/models.py +10 -0
- pyxecm_api/agents/otcm_knowledgegraph/functions.py +85 -0
- pyxecm_api/agents/otcm_knowledgegraph/models.py +61 -0
- pyxecm_api/agents/otcm_knowledgegraph/router.py +74 -0
- pyxecm_api/agents/otcm_user_agent/models.py +20 -0
- pyxecm_api/agents/otcm_user_agent/router.py +65 -0
- pyxecm_api/agents/otcm_workspace_agent/models.py +40 -0
- pyxecm_api/agents/otcm_workspace_agent/router.py +200 -0
- pyxecm_api/app.py +221 -0
- {pyxecm/customizer/api → pyxecm_api}/auth/functions.py +10 -2
- {pyxecm/customizer/api → pyxecm_api}/auth/router.py +4 -3
- {pyxecm/customizer/api → pyxecm_api}/common/functions.py +39 -9
- {pyxecm/customizer/api → pyxecm_api}/common/metrics.py +1 -2
- {pyxecm/customizer/api → pyxecm_api}/common/router.py +7 -8
- {pyxecm/customizer/api → pyxecm_api}/settings.py +21 -6
- {pyxecm/customizer/api → pyxecm_api}/terminal/router.py +1 -1
- {pyxecm/customizer/api → pyxecm_api}/v1_csai/router.py +39 -10
- pyxecm_api/v1_csai/statics/bindings/utils.js +189 -0
- pyxecm_api/v1_csai/statics/tom-select/tom-select.complete.min.js +356 -0
- pyxecm_api/v1_csai/statics/tom-select/tom-select.css +334 -0
- pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.css +1 -0
- pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.min.js +27 -0
- pyxecm_api/v1_maintenance/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/functions.py +3 -3
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/router.py +8 -8
- pyxecm_api/v1_otcs/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_otcs/functions.py +7 -5
- {pyxecm/customizer/api → pyxecm_api}/v1_otcs/router.py +8 -7
- pyxecm_api/v1_payload/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/functions.py +10 -7
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/router.py +11 -10
- {pyxecm/customizer → pyxecm_customizer}/__init__.py +8 -0
- {pyxecm/customizer → pyxecm_customizer}/__main__.py +15 -21
- {pyxecm/customizer → pyxecm_customizer}/browser_automation.py +414 -103
- {pyxecm/customizer → pyxecm_customizer}/customizer.py +178 -116
- {pyxecm/customizer → pyxecm_customizer}/guidewire.py +60 -20
- {pyxecm/customizer → pyxecm_customizer}/k8s.py +4 -4
- pyxecm_customizer/knowledge_graph.py +719 -0
- pyxecm_customizer/log.py +35 -0
- {pyxecm/customizer → pyxecm_customizer}/m365.py +41 -33
- {pyxecm/customizer → pyxecm_customizer}/payload.py +2265 -1933
- {pyxecm/customizer/api/common → pyxecm_customizer}/payload_list.py +18 -55
- {pyxecm/customizer → pyxecm_customizer}/salesforce.py +1 -1
- {pyxecm/customizer → pyxecm_customizer}/sap.py +6 -2
- {pyxecm/customizer → pyxecm_customizer}/servicenow.py +2 -4
- {pyxecm/customizer → pyxecm_customizer}/settings.py +7 -6
- {pyxecm/customizer → pyxecm_customizer}/successfactors.py +40 -28
- {pyxecm/customizer → pyxecm_customizer}/translate.py +1 -1
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/__main__.py +1 -1
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/app.py +14 -8
- pyxecm/customizer/api/app.py +0 -157
- pyxecm/customizer/log.py +0 -107
- pyxecm/customizer/nhc.py +0 -1169
- pyxecm/customizer/openapi.py +0 -258
- pyxecm/customizer/pht.py +0 -1357
- pyxecm-2.0.4.dist-info/METADATA +0 -119
- pyxecm-2.0.4.dist-info/RECORD +0 -78
- pyxecm-2.0.4.dist-info/licenses/LICENSE +0 -202
- pyxecm-2.0.4.dist-info/top_level.txt +0 -1
- {pyxecm/customizer/api → pyxecm_api}/__init__.py +0 -0
- {pyxecm/customizer/api/auth → pyxecm_api/agents/otcm_knowledgegraph}/__init__.py +0 -0
- {pyxecm/customizer/api/common → pyxecm_api/agents/otcm_user_agent}/__init__.py +0 -0
- {pyxecm/customizer/api/v1_csai → pyxecm_api/agents/otcm_workspace_agent}/__init__.py +0 -0
- {pyxecm/customizer/api/v1_maintenance → pyxecm_api/auth}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/auth/models.py +0 -0
- {pyxecm/customizer/api/v1_otcs → pyxecm_api/common}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/common/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/terminal/__init__.py +0 -0
- {pyxecm/customizer/api/v1_payload → pyxecm_api/v1_csai}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_csai/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/models.py +0 -0
- {pyxecm/customizer → pyxecm_customizer}/exceptions.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/__init__.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/settings.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/static/favicon.avif +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/templates/maintenance.html +0 -0
|
@@ -59,6 +59,7 @@ __email__ = "mdiefenb@opentext.com"
|
|
|
59
59
|
|
|
60
60
|
import logging
|
|
61
61
|
import os
|
|
62
|
+
import re
|
|
62
63
|
import subprocess
|
|
63
64
|
import tempfile
|
|
64
65
|
import time
|
|
@@ -66,7 +67,7 @@ import traceback
|
|
|
66
67
|
from http import HTTPStatus
|
|
67
68
|
from types import TracebackType
|
|
68
69
|
|
|
69
|
-
default_logger = logging.getLogger("
|
|
70
|
+
default_logger = logging.getLogger("pyxecm_customizer.browser_automation")
|
|
70
71
|
|
|
71
72
|
# For backwards compatibility we also want to handle
|
|
72
73
|
# cases where the playwright modules have not been installed
|
|
@@ -151,7 +152,7 @@ class BrowserAutomation:
|
|
|
151
152
|
download_directory = os.path.join(
|
|
152
153
|
tempfile.gettempdir(),
|
|
153
154
|
"browser_automations",
|
|
154
|
-
automation_name,
|
|
155
|
+
self.sanitize_filename(filename=automation_name),
|
|
155
156
|
"downloads",
|
|
156
157
|
)
|
|
157
158
|
|
|
@@ -165,10 +166,11 @@ class BrowserAutomation:
|
|
|
165
166
|
self.user_password = user_password
|
|
166
167
|
self.logged_in = False
|
|
167
168
|
self.download_directory = download_directory
|
|
169
|
+
self.headless = headless
|
|
168
170
|
|
|
169
171
|
# Screenshot configurations:
|
|
170
172
|
self.take_screenshots = take_screenshots
|
|
171
|
-
self.screenshot_names = automation_name
|
|
173
|
+
self.screenshot_names = self.sanitize_filename(filename=automation_name)
|
|
172
174
|
self.screenshot_counter = 1
|
|
173
175
|
self.screenshot_full_page = True
|
|
174
176
|
|
|
@@ -177,19 +179,16 @@ class BrowserAutomation:
|
|
|
177
179
|
self.screenshot_directory = os.path.join(
|
|
178
180
|
tempfile.gettempdir(),
|
|
179
181
|
"browser_automations",
|
|
180
|
-
|
|
182
|
+
self.screenshot_names,
|
|
181
183
|
"screenshots",
|
|
182
184
|
)
|
|
183
185
|
self.logger.debug("Creating Screenshot directory... -> %s", self.screenshot_directory)
|
|
184
186
|
if self.take_screenshots and not os.path.exists(self.screenshot_directory):
|
|
185
187
|
os.makedirs(self.screenshot_directory)
|
|
186
188
|
|
|
187
|
-
self.
|
|
188
|
-
self.playwright = sync_playwright().start()
|
|
189
|
-
|
|
190
|
-
proxy = None
|
|
189
|
+
self.proxy = None
|
|
191
190
|
if os.getenv("HTTP_PROXY"):
|
|
192
|
-
proxy = {
|
|
191
|
+
self.proxy = {
|
|
193
192
|
"server": os.getenv("HTTP_PROXY"),
|
|
194
193
|
}
|
|
195
194
|
self.logger.info("Using HTTP proxy -> %s", os.getenv("HTTP_PROXY"))
|
|
@@ -197,71 +196,110 @@ class BrowserAutomation:
|
|
|
197
196
|
browser = browser or os.getenv("BROWSER", "webkit")
|
|
198
197
|
self.logger.info("Using Browser -> '%s'...", browser)
|
|
199
198
|
|
|
199
|
+
if not self.setup_playwright(browser=browser):
|
|
200
|
+
self.logger.error("Failed to initialize Playwright browser automation!")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
self.logger.info("Creating Browser Context...")
|
|
204
|
+
self.context: BrowserContext = self.browser.new_context(
|
|
205
|
+
accept_downloads=True,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.logger.info("Creating Page...")
|
|
209
|
+
self.page: Page = self.context.new_page()
|
|
210
|
+
self.main_page = self.page
|
|
211
|
+
self.logger.info("Browser Automation initialized.")
|
|
212
|
+
|
|
213
|
+
# end method definition
|
|
214
|
+
|
|
215
|
+
def setup_playwright(self, browser: str) -> bool:
|
|
216
|
+
"""Initialize Playwright browser automation.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
browser (str):
|
|
220
|
+
Name of the browser engine.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
bool:
|
|
224
|
+
True = Success, False = Error.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
self.logger.debug("Creating Playwright instance...")
|
|
230
|
+
self.playwright = sync_playwright().start()
|
|
231
|
+
except Exception:
|
|
232
|
+
self.logger.error("Failed to start Playwright!")
|
|
233
|
+
return False
|
|
234
|
+
|
|
200
235
|
match browser:
|
|
201
236
|
case "chromium":
|
|
202
237
|
try:
|
|
203
238
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
204
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
239
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
205
240
|
)
|
|
206
241
|
except Exception:
|
|
207
242
|
self.install_browser(browser=browser)
|
|
208
243
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
209
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
244
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
210
245
|
)
|
|
211
246
|
|
|
212
247
|
case "chrome":
|
|
213
248
|
try:
|
|
214
249
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
215
|
-
channel="chrome",
|
|
250
|
+
channel="chrome",
|
|
251
|
+
headless=self.headless,
|
|
252
|
+
slow_mo=100 if not self.headless else None,
|
|
253
|
+
proxy=self.proxy,
|
|
216
254
|
)
|
|
217
255
|
except Exception:
|
|
218
256
|
self.install_browser(browser=browser)
|
|
219
257
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
220
|
-
channel="chrome",
|
|
258
|
+
channel="chrome",
|
|
259
|
+
headless=self.headless,
|
|
260
|
+
slow_mo=100 if not self.headless else None,
|
|
261
|
+
proxy=self.proxy,
|
|
221
262
|
)
|
|
222
263
|
|
|
223
264
|
case "msedge":
|
|
224
265
|
try:
|
|
225
266
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
226
|
-
channel="msedge",
|
|
267
|
+
channel="msedge",
|
|
268
|
+
headless=self.headless,
|
|
269
|
+
slow_mo=100 if not self.headless else None,
|
|
270
|
+
proxy=self.proxy,
|
|
227
271
|
)
|
|
228
272
|
except Exception:
|
|
229
273
|
self.install_browser(browser=browser)
|
|
230
274
|
self.browser: Browser = self.playwright.chromium.launch(
|
|
231
|
-
channel="msedge",
|
|
275
|
+
channel="msedge",
|
|
276
|
+
headless=self.headless,
|
|
277
|
+
slow_mo=100 if not self.headless else None,
|
|
278
|
+
proxy=self.proxy,
|
|
232
279
|
)
|
|
233
280
|
|
|
234
281
|
case "webkit":
|
|
235
282
|
try:
|
|
236
283
|
self.browser: Browser = self.playwright.webkit.launch(
|
|
237
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
284
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
238
285
|
)
|
|
239
286
|
except Exception:
|
|
240
287
|
self.install_browser(browser=browser)
|
|
241
288
|
self.browser: Browser = self.playwright.webkit.launch(
|
|
242
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
289
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
243
290
|
)
|
|
244
291
|
|
|
245
292
|
case "firefox":
|
|
246
293
|
try:
|
|
247
294
|
self.browser: Browser = self.playwright.firefox.launch(
|
|
248
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
295
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
249
296
|
)
|
|
250
297
|
except Exception:
|
|
251
298
|
self.install_browser(browser=browser)
|
|
252
299
|
self.browser: Browser = self.playwright.firefox.launch(
|
|
253
|
-
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
300
|
+
headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
|
|
254
301
|
)
|
|
255
|
-
|
|
256
|
-
self.logger.info("Creating Browser Context...")
|
|
257
|
-
self.context: BrowserContext = self.browser.new_context(
|
|
258
|
-
accept_downloads=True,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
self.logger.info("Creating Page...")
|
|
262
|
-
self.page: Page = self.context.new_page()
|
|
263
|
-
self.main_page = self.page
|
|
264
|
-
self.logger.info("Browser Automation initialized.")
|
|
302
|
+
return True
|
|
265
303
|
|
|
266
304
|
# end method definition
|
|
267
305
|
|
|
@@ -287,6 +325,32 @@ class BrowserAutomation:
|
|
|
287
325
|
|
|
288
326
|
# end method definition
|
|
289
327
|
|
|
328
|
+
def sanitize_filename(self, filename: str) -> str:
|
|
329
|
+
"""Sanitize a string to be safe for use as a filename.
|
|
330
|
+
|
|
331
|
+
- Replaces spaces with underscores
|
|
332
|
+
- Removes unsafe characters
|
|
333
|
+
- Converts to lowercase
|
|
334
|
+
- Trims length and dots
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
filename (str):
|
|
338
|
+
The filename to sanitize.
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
filename = filename.lower()
|
|
343
|
+
filename = filename.replace(" ", "_")
|
|
344
|
+
filename = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "", filename) # Remove unsafe chars
|
|
345
|
+
filename = re.sub(r"\.+$", "", filename) # Remove trailing dots
|
|
346
|
+
filename = filename.strip()
|
|
347
|
+
if not filename:
|
|
348
|
+
filename = "untitled"
|
|
349
|
+
|
|
350
|
+
return filename
|
|
351
|
+
|
|
352
|
+
# end method definition
|
|
353
|
+
|
|
290
354
|
def take_screenshot(self) -> bool:
|
|
291
355
|
"""Take a screenshot of the current browser window and save it as PNG file.
|
|
292
356
|
|
|
@@ -438,17 +502,29 @@ class BrowserAutomation:
|
|
|
438
502
|
"""
|
|
439
503
|
|
|
440
504
|
if not element:
|
|
441
|
-
self.logger.error("Undefined element!")
|
|
505
|
+
self.logger.error("Undefined element! Cannot scroll to it.")
|
|
442
506
|
return
|
|
443
507
|
|
|
444
508
|
try:
|
|
445
509
|
element.scroll_into_view_if_needed()
|
|
446
510
|
except PlaywrightError as e:
|
|
447
|
-
self.logger.
|
|
511
|
+
self.logger.warning("Cannot scroll element -> %s into view; error -> %s", str(element), str(e))
|
|
448
512
|
|
|
449
513
|
# end method definition
|
|
450
514
|
|
|
451
|
-
def get_locator(
|
|
515
|
+
def get_locator(
|
|
516
|
+
self,
|
|
517
|
+
selector: str,
|
|
518
|
+
selector_type: str,
|
|
519
|
+
role_type: str | None = None,
|
|
520
|
+
exact_match: bool | None = None,
|
|
521
|
+
iframe: str | None = None,
|
|
522
|
+
regex: bool = False,
|
|
523
|
+
filter_has_text: str | None = None,
|
|
524
|
+
filter_has: Locator | None = None,
|
|
525
|
+
filter_has_not_text: str | None = None,
|
|
526
|
+
filter_has_not: Locator | None = None,
|
|
527
|
+
) -> Locator | None:
|
|
452
528
|
"""Determine the locator for the given selector type and (optional) role type.
|
|
453
529
|
|
|
454
530
|
Args:
|
|
@@ -467,10 +543,27 @@ class BrowserAutomation:
|
|
|
467
543
|
role_type (str | None, optional):
|
|
468
544
|
ARIA role when using selector_type="role", e.g., "button", "textbox".
|
|
469
545
|
If irrelevant then None should be passed for role_type.
|
|
546
|
+
exact_match (bool | None, optional):
|
|
547
|
+
Controls whether the text or name must match exactly.
|
|
548
|
+
Default is None (not set, i.e. using playwrights default).
|
|
549
|
+
iframe (str | None):
|
|
550
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
551
|
+
regex (bool, optional):
|
|
552
|
+
Should the name be interpreted as a regular expression?
|
|
553
|
+
filter_has_text (str | None, optional):
|
|
554
|
+
Applies `locator.filter(has_text=...)` to narrow the selection based on text content.
|
|
555
|
+
filter_has (Locator | None, optional):
|
|
556
|
+
Applies `locator.filter(has=...)` to match elements containing a descendant matching the given Locator.
|
|
557
|
+
filter_has_not_text (str | None, optional):
|
|
558
|
+
Applies `locator.filter(has_not_text=...)` to exclude elements with matching text content.
|
|
559
|
+
filter_has_not (Locator | None, optional):
|
|
560
|
+
Applies `locator.filter(has_not=...)` to exclude elements containing a matching descendant.
|
|
470
561
|
|
|
471
562
|
"""
|
|
472
563
|
|
|
473
564
|
try:
|
|
565
|
+
name_or_text = re.compile(selector) if regex else selector
|
|
566
|
+
|
|
474
567
|
match selector_type:
|
|
475
568
|
case "id":
|
|
476
569
|
locator = self.page.locator("#{}".format(selector))
|
|
@@ -481,27 +574,50 @@ class BrowserAutomation:
|
|
|
481
574
|
case "xpath":
|
|
482
575
|
locator = self.page.locator("xpath={}".format(selector))
|
|
483
576
|
case "css":
|
|
484
|
-
|
|
577
|
+
if iframe is None:
|
|
578
|
+
locator = self.page.locator(selector)
|
|
579
|
+
else:
|
|
580
|
+
locator = self.page.locator("iframe[name='{}']".format(iframe)).content_frame.locator(selector)
|
|
485
581
|
case "text":
|
|
486
|
-
|
|
487
|
-
|
|
582
|
+
if iframe is None:
|
|
583
|
+
locator = self.page.get_by_text(text=name_or_text)
|
|
584
|
+
else:
|
|
585
|
+
locator = self.page.locator("iframe[name='{}']".format(iframe)).content_frame.get_by_text(
|
|
586
|
+
name_or_text
|
|
587
|
+
)
|
|
488
588
|
case "title":
|
|
489
|
-
locator = self.page.get_by_title(
|
|
589
|
+
locator = self.page.get_by_title(text=name_or_text)
|
|
490
590
|
case "label":
|
|
491
|
-
locator = self.page.get_by_label(
|
|
591
|
+
locator = self.page.get_by_label(text=name_or_text)
|
|
492
592
|
case "placeholder":
|
|
493
|
-
locator = self.page.get_by_placeholder(
|
|
593
|
+
locator = self.page.get_by_placeholder(text=name_or_text)
|
|
494
594
|
case "alt":
|
|
495
|
-
locator = self.page.get_by_alt_text(
|
|
595
|
+
locator = self.page.get_by_alt_text(text=name_or_text)
|
|
496
596
|
case "role":
|
|
497
597
|
if not role_type:
|
|
498
598
|
self.logger.error("Role type must be specified when using find method 'role'!")
|
|
499
599
|
return None
|
|
500
|
-
|
|
600
|
+
if iframe is None:
|
|
601
|
+
if regex:
|
|
602
|
+
locator = self.page.get_by_role(role=role_type, name=name_or_text)
|
|
603
|
+
else:
|
|
604
|
+
locator = self.page.get_by_role(role=role_type, name=selector, exact=exact_match)
|
|
605
|
+
else:
|
|
606
|
+
content_frame = self.page.locator("iframe[name='{}']".format(iframe)).content_frame
|
|
607
|
+
if regex:
|
|
608
|
+
locator = content_frame.get_by_role(role=role_type, name=name_or_text)
|
|
609
|
+
else:
|
|
610
|
+
locator = content_frame.get_by_role(role=role_type, name=selector, exact=exact_match)
|
|
501
611
|
case _:
|
|
502
612
|
self.logger.error("Unsupported selector type -> '%s'", selector_type)
|
|
503
613
|
return None
|
|
504
614
|
|
|
615
|
+
# Apply filter if needed
|
|
616
|
+
if any([filter_has_text, filter_has, filter_has_not_text, filter_has_not]):
|
|
617
|
+
locator = locator.filter(
|
|
618
|
+
has_text=filter_has_text, has=filter_has, has_not_text=filter_has_not_text, has_not=filter_has_not
|
|
619
|
+
)
|
|
620
|
+
|
|
505
621
|
except PlaywrightError as e:
|
|
506
622
|
self.logger.error("Failure to determine page locator; error -> %s", str(e))
|
|
507
623
|
return None
|
|
@@ -516,6 +632,11 @@ class BrowserAutomation:
|
|
|
516
632
|
selector_type: str = "id",
|
|
517
633
|
role_type: str | None = None,
|
|
518
634
|
wait_state: str = "visible",
|
|
635
|
+
exact_match: bool | None = None,
|
|
636
|
+
regex: bool = False,
|
|
637
|
+
iframe: str | None = None,
|
|
638
|
+
repeat_reload: int | None = None,
|
|
639
|
+
repeat_reload_delay: int = 60,
|
|
519
640
|
show_error: bool = True,
|
|
520
641
|
) -> Locator | None:
|
|
521
642
|
"""Find a page element.
|
|
@@ -532,51 +653,106 @@ class BrowserAutomation:
|
|
|
532
653
|
wait_state (str, optional):
|
|
533
654
|
Defines if we wait for attached (element is part of DOM) or
|
|
534
655
|
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
656
|
+
exact_match (bool | None, optional):
|
|
657
|
+
If an exact matching is required. Default is None (not set).
|
|
658
|
+
regex (bool, optional):
|
|
659
|
+
Should the name be interpreted as a regular expression?
|
|
660
|
+
iframe (str | None):
|
|
661
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
662
|
+
repeat_reload (int | None):
|
|
663
|
+
For pages that are not dynamically updated and require a reload to show an update
|
|
664
|
+
a number of page reloads can be configured.
|
|
665
|
+
repeat_reload_delay (float | None):
|
|
666
|
+
Number of seconds to wait.
|
|
535
667
|
show_error (bool, optional):
|
|
536
668
|
Show an error if not found or not visible.
|
|
537
669
|
|
|
670
|
+
|
|
538
671
|
Returns:
|
|
539
672
|
Locator:
|
|
540
673
|
The web element or None in case an error occured.
|
|
541
674
|
|
|
542
675
|
"""
|
|
543
676
|
|
|
544
|
-
failure_message = "Cannot find page element with selector -> '{}' ({}){}".format(
|
|
545
|
-
selector,
|
|
677
|
+
failure_message = "Cannot find page element with selector -> '{}' ({}){}{}".format(
|
|
678
|
+
selector,
|
|
679
|
+
selector_type,
|
|
680
|
+
" and role type -> '{}'".format(role_type) if role_type else "",
|
|
681
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
546
682
|
)
|
|
547
|
-
success_message = "Found page element with selector -> '{}' ('{}'){}".format(
|
|
548
|
-
selector,
|
|
683
|
+
success_message = "Found page element with selector -> '{}' ('{}'){}{}".format(
|
|
684
|
+
selector,
|
|
685
|
+
selector_type,
|
|
686
|
+
" and role type -> '{}'".format(role_type) if role_type else "",
|
|
687
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
549
688
|
)
|
|
550
689
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
690
|
+
def do_find() -> Locator | None:
|
|
691
|
+
# Determine the locator for the element:
|
|
692
|
+
locator = self.get_locator(
|
|
693
|
+
selector=selector,
|
|
694
|
+
selector_type=selector_type,
|
|
695
|
+
role_type=role_type,
|
|
696
|
+
exact_match=exact_match,
|
|
697
|
+
iframe=iframe,
|
|
698
|
+
regex=regex,
|
|
699
|
+
)
|
|
700
|
+
if not locator:
|
|
701
|
+
if show_error:
|
|
702
|
+
self.logger.error(failure_message)
|
|
703
|
+
else:
|
|
704
|
+
self.logger.warning(failure_message)
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
# Wait for the element to be visible - don't use logic like
|
|
708
|
+
# locator.count() as this does not wait but fail immideately if elements
|
|
709
|
+
# are not yet loaded:
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
self.logger.debug(
|
|
713
|
+
"Wait for locator to find element with selector -> '%s' (%s%s%s) and state -> '%s'%s...",
|
|
714
|
+
selector,
|
|
715
|
+
"selector type -> '{}'".format(selector_type),
|
|
716
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
717
|
+
", using regular expression" if regex else "",
|
|
718
|
+
wait_state,
|
|
719
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
720
|
+
)
|
|
721
|
+
locator = locator.first
|
|
722
|
+
locator.wait_for(state=wait_state)
|
|
723
|
+
except PlaywrightError as pe:
|
|
724
|
+
if show_error and repeat_reload is None:
|
|
725
|
+
self.logger.error("%s (%s)", failure_message, str(pe))
|
|
726
|
+
else:
|
|
727
|
+
self.logger.warning("%s", failure_message)
|
|
728
|
+
return None
|
|
556
729
|
else:
|
|
557
|
-
self.logger.
|
|
558
|
-
return None
|
|
730
|
+
self.logger.debug(success_message)
|
|
559
731
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
#
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
732
|
+
return locator
|
|
733
|
+
|
|
734
|
+
# end def do_find():
|
|
735
|
+
|
|
736
|
+
locator = do_find()
|
|
737
|
+
|
|
738
|
+
# Retry logic for pages that are not updated dynamically:
|
|
739
|
+
if locator is None and repeat_reload is not None:
|
|
740
|
+
for i in range(repeat_reload):
|
|
741
|
+
self.logger.warning(
|
|
742
|
+
"Wait %f seconds before reloading page -> %s to retrieve updates from server...",
|
|
743
|
+
repeat_reload_delay,
|
|
744
|
+
self.page.url,
|
|
745
|
+
)
|
|
746
|
+
time.sleep(repeat_reload_delay)
|
|
747
|
+
self.logger.warning(
|
|
748
|
+
"Reloading page -> %s (retry %d) to retrieve updates from server...", self.page.url, i + 1
|
|
749
|
+
)
|
|
750
|
+
self.page.reload()
|
|
751
|
+
locator = do_find()
|
|
752
|
+
if locator:
|
|
753
|
+
break
|
|
575
754
|
else:
|
|
576
|
-
self.logger.
|
|
577
|
-
return None
|
|
578
|
-
else:
|
|
579
|
-
self.logger.debug(success_message)
|
|
755
|
+
self.logger.error(failure_message)
|
|
580
756
|
|
|
581
757
|
return locator
|
|
582
758
|
|
|
@@ -594,6 +770,16 @@ class BrowserAutomation:
|
|
|
594
770
|
is_page_close_trigger: bool = False,
|
|
595
771
|
wait_until: str | None = None,
|
|
596
772
|
wait_time: float = 0.0,
|
|
773
|
+
exact_match: bool | None = None,
|
|
774
|
+
regex: bool = False,
|
|
775
|
+
hover_only: bool = False,
|
|
776
|
+
iframe: str | None = None,
|
|
777
|
+
force: bool | None = None,
|
|
778
|
+
click_button: str | None = None,
|
|
779
|
+
click_count: int | None = None,
|
|
780
|
+
click_modifiers: list | None = None,
|
|
781
|
+
repeat_reload: int | None = None,
|
|
782
|
+
repeat_reload_delay: float = 60.0,
|
|
597
783
|
show_error: bool = True,
|
|
598
784
|
) -> bool:
|
|
599
785
|
"""Find a page element and click it.
|
|
@@ -630,6 +816,32 @@ class BrowserAutomation:
|
|
|
630
816
|
but subresources may still load).
|
|
631
817
|
wait_time (float):
|
|
632
818
|
Time in seconds to wait for elements to appear.
|
|
819
|
+
exact_match (bool | None, optional):
|
|
820
|
+
If an exact matching is required. Default is None (not set).
|
|
821
|
+
regex (bool, optional):
|
|
822
|
+
Should the name be interpreted as a regular expression?
|
|
823
|
+
hover_only (bool, optional):
|
|
824
|
+
Should we only hover over the element and not click it? Helpful for
|
|
825
|
+
menus that are opening on hovering.
|
|
826
|
+
iframe (str | None, optional):
|
|
827
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
828
|
+
force (bool | None, optional):
|
|
829
|
+
If sure the element is interactable and visible (even partly), you can bypass visibility checks
|
|
830
|
+
by setting this option to True. Default is None (undefined, i.e. using the playwright default which is False)
|
|
831
|
+
click_button (Literal['left', 'middle', 'right'] | None, optional):
|
|
832
|
+
Which mouse button to use to do the click. The default is "left". This will be used by playwright if None
|
|
833
|
+
is passed.
|
|
834
|
+
click_count (int | None, optional):
|
|
835
|
+
Number of clicks. E.g. 2 for a "double-click".
|
|
836
|
+
click_modifiers (list | None, optional):
|
|
837
|
+
Key pressed together with the mouse click.
|
|
838
|
+
Possible values:'Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'.
|
|
839
|
+
Default is None = no key pressed.
|
|
840
|
+
repeat_reload (int | None):
|
|
841
|
+
For pages that are not dynamically updated and require a reload to show an update
|
|
842
|
+
a number of page reloads can be configured.
|
|
843
|
+
repeat_reload_delay (float | None):
|
|
844
|
+
Number of seconds to wait.
|
|
633
845
|
show_error (bool, optional):
|
|
634
846
|
Show an error if the element is not found or not clickable.
|
|
635
847
|
|
|
@@ -662,7 +874,15 @@ class BrowserAutomation:
|
|
|
662
874
|
return False
|
|
663
875
|
|
|
664
876
|
elem = self.find_elem(
|
|
665
|
-
selector=selector,
|
|
877
|
+
selector=selector,
|
|
878
|
+
selector_type=selector_type,
|
|
879
|
+
role_type=role_type,
|
|
880
|
+
exact_match=exact_match,
|
|
881
|
+
regex=regex,
|
|
882
|
+
iframe=iframe,
|
|
883
|
+
repeat_reload=repeat_reload,
|
|
884
|
+
repeat_reload_delay=repeat_reload_delay,
|
|
885
|
+
show_error=show_error,
|
|
666
886
|
)
|
|
667
887
|
if not elem:
|
|
668
888
|
return not show_error
|
|
@@ -680,7 +900,7 @@ class BrowserAutomation:
|
|
|
680
900
|
else:
|
|
681
901
|
# Will this click trigger a naviagation?
|
|
682
902
|
if is_navigation_trigger:
|
|
683
|
-
self.logger.
|
|
903
|
+
self.logger.debug(
|
|
684
904
|
"Clicking on navigation-triggering element -> '%s' (%s%s) and wait until -> '%s'...",
|
|
685
905
|
selector,
|
|
686
906
|
"selector type -> '{}'".format(selector_type),
|
|
@@ -688,29 +908,38 @@ class BrowserAutomation:
|
|
|
688
908
|
wait_until,
|
|
689
909
|
)
|
|
690
910
|
with self.page.expect_navigation(wait_until=wait_until):
|
|
691
|
-
elem.click()
|
|
911
|
+
elem.click(force=force, button=click_button, click_count=click_count, modifiers=click_modifiers)
|
|
692
912
|
# Will this click trigger a a new popup window?
|
|
693
913
|
elif is_popup_trigger:
|
|
694
914
|
with self.page.expect_popup() as popup_info:
|
|
695
|
-
elem.click()
|
|
915
|
+
elem.click(force=force, button=click_button, click_count=click_count, modifiers=click_modifiers)
|
|
696
916
|
if not popup_info or not popup_info.value:
|
|
697
917
|
self.logger.info("Popup window did not open as expected!")
|
|
698
918
|
success = False
|
|
699
919
|
else:
|
|
700
920
|
self.page = popup_info.value
|
|
701
921
|
self.logger.info("Move browser automation to popup window -> %s...", self.page.url)
|
|
922
|
+
elif hover_only:
|
|
923
|
+
self.logger.debug(
|
|
924
|
+
"Hovering over element -> '%s' (%s%s)...",
|
|
925
|
+
selector,
|
|
926
|
+
"selector type -> '{}'".format(selector_type),
|
|
927
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
928
|
+
)
|
|
929
|
+
elem.hover()
|
|
702
930
|
else:
|
|
703
|
-
self.logger.
|
|
931
|
+
self.logger.debug(
|
|
704
932
|
"Clicking on non-navigating element -> '%s' (%s%s)...",
|
|
705
933
|
selector,
|
|
706
934
|
"selector type -> '{}'".format(selector_type),
|
|
707
935
|
", role type -> '{}'".format(role_type) if role_type else "",
|
|
708
936
|
)
|
|
709
|
-
elem.click()
|
|
937
|
+
elem.click(force=force, button=click_button, click_count=click_count, modifiers=click_modifiers)
|
|
710
938
|
time.sleep(1)
|
|
711
939
|
if success:
|
|
712
940
|
self.logger.debug(
|
|
713
|
-
"Successfully
|
|
941
|
+
"Successfully %s element -> '%s' (%s%s)",
|
|
942
|
+
"clicked" if not hover_only else "hovered over",
|
|
714
943
|
selector,
|
|
715
944
|
"selector type -> '{}'".format(selector_type),
|
|
716
945
|
", role type -> '{}'".format(role_type) if role_type else "",
|
|
@@ -749,6 +978,11 @@ class BrowserAutomation:
|
|
|
749
978
|
selector_type: str = "id",
|
|
750
979
|
role_type: str | None = None,
|
|
751
980
|
is_sensitive: bool = False,
|
|
981
|
+
press_enter: bool = False,
|
|
982
|
+
exact_match: bool | None = None,
|
|
983
|
+
regex: bool = False,
|
|
984
|
+
iframe: str | None = None,
|
|
985
|
+
typing: bool = False,
|
|
752
986
|
show_error: bool = True,
|
|
753
987
|
) -> bool:
|
|
754
988
|
"""Find an page element and fill it with a new text.
|
|
@@ -766,6 +1000,17 @@ class BrowserAutomation:
|
|
|
766
1000
|
If irrelevant then None should be passed for role_type.
|
|
767
1001
|
is_sensitive (bool, optional):
|
|
768
1002
|
True for suppressing sensitive information in logging.
|
|
1003
|
+
press_enter (bool, optional):
|
|
1004
|
+
Whether or not to press "Enter" after entering
|
|
1005
|
+
exact_match (bool | None, optional):
|
|
1006
|
+
If an exact matching is required. Default is None (not set).
|
|
1007
|
+
regex (bool, optional):
|
|
1008
|
+
Should the name be interpreted as a regular expression?
|
|
1009
|
+
iframe (str | None):
|
|
1010
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
1011
|
+
typing (bool, optional):
|
|
1012
|
+
Not just set the value of the elem but simulate real typing.
|
|
1013
|
+
This is required for pages with fields that do react in a "typeahead" manner.
|
|
769
1014
|
show_error (bool, optional):
|
|
770
1015
|
Show an error if the element is not found or not clickable.
|
|
771
1016
|
|
|
@@ -777,9 +1022,17 @@ class BrowserAutomation:
|
|
|
777
1022
|
|
|
778
1023
|
success = False # Final return value
|
|
779
1024
|
|
|
780
|
-
elem = self.find_elem(
|
|
1025
|
+
elem = self.find_elem(
|
|
1026
|
+
selector=selector,
|
|
1027
|
+
selector_type=selector_type,
|
|
1028
|
+
role_type=role_type,
|
|
1029
|
+
exact_match=exact_match,
|
|
1030
|
+
regex=regex,
|
|
1031
|
+
iframe=iframe,
|
|
1032
|
+
show_error=True,
|
|
1033
|
+
)
|
|
781
1034
|
if not elem:
|
|
782
|
-
return
|
|
1035
|
+
return not show_error
|
|
783
1036
|
|
|
784
1037
|
is_enabled = elem.is_enabled()
|
|
785
1038
|
if not is_enabled:
|
|
@@ -837,7 +1090,12 @@ class BrowserAutomation:
|
|
|
837
1090
|
|
|
838
1091
|
success = retry < 5 # True is less than 5 retries were needed
|
|
839
1092
|
else:
|
|
840
|
-
|
|
1093
|
+
if typing:
|
|
1094
|
+
elem.type(value, delay=50)
|
|
1095
|
+
else:
|
|
1096
|
+
elem.fill(value)
|
|
1097
|
+
if press_enter:
|
|
1098
|
+
self.page.keyboard.press("Enter")
|
|
841
1099
|
success = True
|
|
842
1100
|
except PlaywrightError as e:
|
|
843
1101
|
message = "Cannot set page element selected by -> '{}' ({}) to value -> '{}'; error -> {}".format(
|
|
@@ -861,6 +1119,9 @@ class BrowserAutomation:
|
|
|
861
1119
|
selector: str,
|
|
862
1120
|
selector_type: str = "id",
|
|
863
1121
|
role_type: str | None = None,
|
|
1122
|
+
exact_match: bool | None = None,
|
|
1123
|
+
regex: bool = False,
|
|
1124
|
+
iframe: str | None = None,
|
|
864
1125
|
download_time: int = 30,
|
|
865
1126
|
) -> str | None:
|
|
866
1127
|
"""Click a page element to initiate a download.
|
|
@@ -874,6 +1135,12 @@ class BrowserAutomation:
|
|
|
874
1135
|
role_type (str | None, optional):
|
|
875
1136
|
ARIA role when using selector_type="role", e.g., "button", "textbox".
|
|
876
1137
|
If irrelevant then None should be passed for role_type.
|
|
1138
|
+
exact_match (bool | None, optional):
|
|
1139
|
+
If an exact matching is required. Default is None (not set).
|
|
1140
|
+
regex (bool, optional):
|
|
1141
|
+
Should the name be interpreted as a regular expression?
|
|
1142
|
+
iframe (str | None):
|
|
1143
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
877
1144
|
download_time (int, optional):
|
|
878
1145
|
Time in seconds to wait for the download to complete.
|
|
879
1146
|
|
|
@@ -885,7 +1152,14 @@ class BrowserAutomation:
|
|
|
885
1152
|
|
|
886
1153
|
try:
|
|
887
1154
|
with self.page.expect_download(timeout=download_time * 1000) as download_info:
|
|
888
|
-
clicked = self.find_elem_and_click(
|
|
1155
|
+
clicked = self.find_elem_and_click(
|
|
1156
|
+
selector=selector,
|
|
1157
|
+
selector_type=selector_type,
|
|
1158
|
+
role_type=role_type,
|
|
1159
|
+
exact_match=exact_match,
|
|
1160
|
+
regex=regex,
|
|
1161
|
+
iframe=iframe,
|
|
1162
|
+
)
|
|
889
1163
|
if not clicked:
|
|
890
1164
|
self.logger.error("Element not found to initiate download.")
|
|
891
1165
|
return None
|
|
@@ -910,8 +1184,10 @@ class BrowserAutomation:
|
|
|
910
1184
|
selector_type: str = "id",
|
|
911
1185
|
role_type: str | None = None,
|
|
912
1186
|
value: str | None = None,
|
|
1187
|
+
exact_match: bool | None = None,
|
|
913
1188
|
attribute: str | None = None,
|
|
914
1189
|
substring: bool = True,
|
|
1190
|
+
iframe: str | None = None,
|
|
915
1191
|
min_count: int = 1,
|
|
916
1192
|
wait_time: float = 0.0,
|
|
917
1193
|
wait_state: str = "visible",
|
|
@@ -937,10 +1213,14 @@ class BrowserAutomation:
|
|
|
937
1213
|
If irrelevant then None should be passed for role_type.
|
|
938
1214
|
value (str, optional):
|
|
939
1215
|
Value to match in attribute or element content.
|
|
1216
|
+
exact_match (bool | None, optional):
|
|
1217
|
+
If an exact matching is required. Default is None (not set).
|
|
940
1218
|
attribute (str, optional):
|
|
941
1219
|
Attribute name to inspect. If None, uses element's text.
|
|
942
1220
|
substring (bool):
|
|
943
1221
|
If True, allow partial match.
|
|
1222
|
+
iframe (str | None):
|
|
1223
|
+
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
944
1224
|
min_count (int):
|
|
945
1225
|
Minimum number of required matches (# elements on page).
|
|
946
1226
|
wait_time (float):
|
|
@@ -960,12 +1240,21 @@ class BrowserAutomation:
|
|
|
960
1240
|
|
|
961
1241
|
"""
|
|
962
1242
|
|
|
963
|
-
failure_message = "No matching page element found with selector -> '{}' ({}){}".format(
|
|
964
|
-
selector,
|
|
1243
|
+
failure_message = "No matching page element found with selector -> '{}' ({}){}{}".format(
|
|
1244
|
+
selector,
|
|
1245
|
+
selector_type,
|
|
1246
|
+
" and role type -> '{}'".format(role_type) if role_type else "",
|
|
1247
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
965
1248
|
)
|
|
966
1249
|
|
|
967
1250
|
# Determine the locator for the elements:
|
|
968
|
-
locator = self.get_locator(
|
|
1251
|
+
locator = self.get_locator(
|
|
1252
|
+
selector=selector,
|
|
1253
|
+
selector_type=selector_type,
|
|
1254
|
+
role_type=role_type,
|
|
1255
|
+
exact_match=exact_match,
|
|
1256
|
+
iframe=iframe,
|
|
1257
|
+
)
|
|
969
1258
|
if not locator:
|
|
970
1259
|
if show_error:
|
|
971
1260
|
self.logger.error(
|
|
@@ -974,7 +1263,7 @@ class BrowserAutomation:
|
|
|
974
1263
|
return (None, 0)
|
|
975
1264
|
|
|
976
1265
|
self.logger.info(
|
|
977
|
-
"Check if at least %d element%s found by selector -> %s (%s%s)%s%s...",
|
|
1266
|
+
"Check if at least %d element%s found by selector -> %s (%s%s)%s%s%s...",
|
|
978
1267
|
min_count,
|
|
979
1268
|
"s are" if min_count > 1 else " is",
|
|
980
1269
|
selector,
|
|
@@ -982,24 +1271,22 @@ class BrowserAutomation:
|
|
|
982
1271
|
", role type -> {}".format(role_type) if role_type else "",
|
|
983
1272
|
" with value -> '{}'".format(value) if value else "",
|
|
984
1273
|
" in attribute -> '{}'".format(attribute) if attribute and value else "",
|
|
1274
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
985
1275
|
)
|
|
986
1276
|
|
|
987
1277
|
# Wait for the element to be visible - don't immediately use logic like
|
|
988
1278
|
# locator.count() as this does not wait but then fail immideately
|
|
989
1279
|
try:
|
|
990
1280
|
self.logger.info(
|
|
991
|
-
"Wait for locator to find first matching element with selector -> '%s' (%s%s) and state -> '%s'...",
|
|
1281
|
+
"Wait for locator to find first matching element with selector -> '%s' (%s%s) and state -> '%s'%s...",
|
|
992
1282
|
selector,
|
|
993
1283
|
"selector type -> '{}'".format(selector_type),
|
|
994
1284
|
", role type -> {}".format(role_type) if role_type else "",
|
|
995
1285
|
wait_state,
|
|
1286
|
+
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
996
1287
|
)
|
|
997
1288
|
self.logger.info("Locator count before waiting: %d", locator.count())
|
|
998
1289
|
|
|
999
|
-
# for i in range(locator.count()):
|
|
1000
|
-
# elem = locator.nth(i)
|
|
1001
|
-
# self.logger.info("Element #%d visible: %s", i, elem.is_visible())
|
|
1002
|
-
|
|
1003
1290
|
# IMPORTANT: We wait for the FIRST element. otherwise we get errors like
|
|
1004
1291
|
# 'Locator.wait_for: Error: strict mode violation'.
|
|
1005
1292
|
# IMPORTANT: if the first match does not comply to the
|
|
@@ -1021,12 +1308,14 @@ class BrowserAutomation:
|
|
|
1021
1308
|
self.logger.info("Wait additional %d milliseconds before checking...", wait_time * 1000)
|
|
1022
1309
|
self.page.wait_for_timeout(wait_time * 1000)
|
|
1023
1310
|
|
|
1024
|
-
matching_elems = []
|
|
1025
|
-
|
|
1026
1311
|
count = locator.count()
|
|
1027
1312
|
if count == 0:
|
|
1028
1313
|
if show_error:
|
|
1029
1314
|
self.logger.error("No elements found using selector -> '%s' ('%s')", selector, selector_type)
|
|
1315
|
+
|
|
1316
|
+
if self.take_screenshots:
|
|
1317
|
+
self.take_screenshot()
|
|
1318
|
+
|
|
1030
1319
|
return (None, 0)
|
|
1031
1320
|
|
|
1032
1321
|
self.logger.info(
|
|
@@ -1045,13 +1334,18 @@ class BrowserAutomation:
|
|
|
1045
1334
|
value,
|
|
1046
1335
|
)
|
|
1047
1336
|
|
|
1337
|
+
matching_elems = []
|
|
1338
|
+
|
|
1339
|
+
# Iterate over all elements found by the locator and checkif
|
|
1340
|
+
# they comply with the additional value conditions (if provided).
|
|
1341
|
+
# We collect all matching elements in a list:
|
|
1048
1342
|
for i in range(count):
|
|
1049
1343
|
elem = locator.nth(i)
|
|
1050
1344
|
if not elem:
|
|
1051
1345
|
continue
|
|
1052
1346
|
|
|
1053
1347
|
if value is None:
|
|
1054
|
-
#
|
|
1348
|
+
# If value is None we do no filtering, accept all elements:
|
|
1055
1349
|
matching_elems.append(elem)
|
|
1056
1350
|
continue
|
|
1057
1351
|
|
|
@@ -1059,15 +1353,15 @@ class BrowserAutomation:
|
|
|
1059
1353
|
attr_value = elem.get_attribute(attribute) if attribute else elem.text_content()
|
|
1060
1354
|
|
|
1061
1355
|
if not attr_value:
|
|
1356
|
+
# Nothing to compare with - continue:
|
|
1062
1357
|
continue
|
|
1063
1358
|
|
|
1359
|
+
# If substring is True we check with "in" otherwise we use the eual operator (==):
|
|
1064
1360
|
if (substring and value in attr_value) or (not substring and value == attr_value):
|
|
1065
1361
|
matching_elems.append(elem)
|
|
1066
1362
|
|
|
1067
1363
|
matching_elements_count = len(matching_elems)
|
|
1068
1364
|
|
|
1069
|
-
success = True
|
|
1070
|
-
|
|
1071
1365
|
if matching_elements_count < min_count:
|
|
1072
1366
|
success = False
|
|
1073
1367
|
if show_error:
|
|
@@ -1077,7 +1371,20 @@ class BrowserAutomation:
|
|
|
1077
1371
|
"s" if matching_elements_count > 1 else "",
|
|
1078
1372
|
min_count,
|
|
1079
1373
|
)
|
|
1080
|
-
|
|
1374
|
+
else:
|
|
1375
|
+
success = True
|
|
1376
|
+
self.logger.info(
|
|
1377
|
+
"Found %d matching elements.%s",
|
|
1378
|
+
matching_elements_count,
|
|
1379
|
+
" This is {} the minimum {} element{} probed for.".format(
|
|
1380
|
+
"exactly" if matching_elements_count == min_count else "more than",
|
|
1381
|
+
min_count,
|
|
1382
|
+
"s" if min_count > 1 else "",
|
|
1383
|
+
),
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
if self.take_screenshots:
|
|
1387
|
+
self.take_screenshot()
|
|
1081
1388
|
|
|
1082
1389
|
return (success, matching_elements_count)
|
|
1083
1390
|
|
|
@@ -1119,7 +1426,8 @@ class BrowserAutomation:
|
|
|
1119
1426
|
Default is "id".
|
|
1120
1427
|
|
|
1121
1428
|
Returns:
|
|
1122
|
-
bool:
|
|
1429
|
+
bool:
|
|
1430
|
+
True = success, False = error.
|
|
1123
1431
|
|
|
1124
1432
|
"""
|
|
1125
1433
|
|
|
@@ -1147,7 +1455,7 @@ class BrowserAutomation:
|
|
|
1147
1455
|
)
|
|
1148
1456
|
return False
|
|
1149
1457
|
|
|
1150
|
-
self.logger.
|
|
1458
|
+
self.logger.debug("Wait for -> '%s' to assure login is completed and target page is loaded.", wait_until)
|
|
1151
1459
|
self.page.wait_for_load_state(wait_until)
|
|
1152
1460
|
|
|
1153
1461
|
title = self.get_title()
|
|
@@ -1208,7 +1516,7 @@ class BrowserAutomation:
|
|
|
1208
1516
|
# end method definition
|
|
1209
1517
|
|
|
1210
1518
|
def __enter__(self) -> object:
|
|
1211
|
-
"""Enable use with 'with' statement."""
|
|
1519
|
+
"""Enable use with 'with' statement (context manager block)."""
|
|
1212
1520
|
|
|
1213
1521
|
return self
|
|
1214
1522
|
|
|
@@ -1217,15 +1525,18 @@ class BrowserAutomation:
|
|
|
1217
1525
|
def __exit__(
|
|
1218
1526
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback_obj: TracebackType | None
|
|
1219
1527
|
) -> None:
|
|
1220
|
-
"""Handle cleanup when exiting a context manager block.
|
|
1528
|
+
"""Handle cleanup when exiting a context manager block ('with' statement).
|
|
1221
1529
|
|
|
1222
1530
|
Ensures all browser-related resources are released. If an unhandled exception
|
|
1223
1531
|
occurs within the context block, it will be logged before cleanup.
|
|
1224
1532
|
|
|
1225
1533
|
Args:
|
|
1226
|
-
exc_type (type[BaseException] | None):
|
|
1227
|
-
|
|
1228
|
-
|
|
1534
|
+
exc_type (type[BaseException] | None):
|
|
1535
|
+
The class of the raised exception, if any.
|
|
1536
|
+
exc_value (BaseException | None):
|
|
1537
|
+
The exception instance raised, if any.
|
|
1538
|
+
traceback_obj (TracebackType | None):
|
|
1539
|
+
The traceback object associated with the exception, if any.
|
|
1229
1540
|
|
|
1230
1541
|
"""
|
|
1231
1542
|
|