pyxecm 2.0.1__py3-none-any.whl → 2.0.3__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/__init__.py +3 -2
- pyxecm/avts.py +3 -1
- pyxecm/customizer/api/app.py +2 -2
- pyxecm/customizer/api/auth/functions.py +37 -30
- pyxecm/customizer/api/common/functions.py +54 -0
- pyxecm/customizer/api/common/router.py +50 -3
- pyxecm/customizer/api/settings.py +5 -3
- pyxecm/customizer/api/terminal/router.py +43 -18
- pyxecm/customizer/api/v1_csai/models.py +18 -0
- pyxecm/customizer/api/v1_csai/router.py +26 -1
- pyxecm/customizer/api/v1_payload/functions.py +9 -3
- pyxecm/customizer/browser_automation.py +508 -200
- pyxecm/customizer/customizer.py +123 -22
- pyxecm/customizer/guidewire.py +170 -37
- pyxecm/customizer/payload.py +614 -257
- pyxecm/customizer/settings.py +21 -3
- pyxecm/helper/xml.py +1 -1
- pyxecm/otawp.py +10 -6
- pyxecm/otca.py +187 -21
- pyxecm/otcs.py +496 -206
- pyxecm/otds.py +1 -0
- pyxecm/otkd.py +1369 -0
- pyxecm/otmm.py +190 -66
- {pyxecm-2.0.1.dist-info → pyxecm-2.0.3.dist-info}/METADATA +3 -6
- {pyxecm-2.0.1.dist-info → pyxecm-2.0.3.dist-info}/RECORD +28 -26
- {pyxecm-2.0.1.dist-info → pyxecm-2.0.3.dist-info}/WHEEL +1 -1
- {pyxecm-2.0.1.dist-info → pyxecm-2.0.3.dist-info}/licenses/LICENSE +0 -0
- {pyxecm-2.0.1.dist-info → pyxecm-2.0.3.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,30 @@ These are typically used as fallback options if no REST API or LLConfig can be u
|
|
|
5
5
|
This module uses playwright: https://playwright.dev for broweser-based automation
|
|
6
6
|
and testing.
|
|
7
7
|
|
|
8
|
+
Core Playwright data types and their relationships:
|
|
9
|
+
|
|
10
|
+
Playwright
|
|
11
|
+
└── BrowserType (chromium / firefox / webkit)
|
|
12
|
+
└── Browser
|
|
13
|
+
└── BrowserContext
|
|
14
|
+
└── Page
|
|
15
|
+
├── Frame
|
|
16
|
+
├── Locator (preferred for element interactions)
|
|
17
|
+
├── ElementHandle (lower-level DOM reference)
|
|
18
|
+
└── JSHandle (handle to any JS object)
|
|
19
|
+
|
|
20
|
+
| Type | Description |
|
|
21
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| **Playwright** | Entry point via `sync_playwright()` or `async_playwright()`. Gives access to browser types. |
|
|
23
|
+
| **BrowserType** | Represents Chromium, Firefox, or WebKit. Used to launch a `Browser`. |
|
|
24
|
+
| **Browser** | A running browser instance. Use `.new_context()` to create sessions. |
|
|
25
|
+
| **BrowserContext** | Isolated incognito-like browser profile. Contains pages. |
|
|
26
|
+
| **Page** | A single browser tab. Main interface for navigation and interaction. |
|
|
27
|
+
| **Frame** | Represents a `<frame>` or `<iframe>`. Like a mini `Page`. |
|
|
28
|
+
| **Locator** | Lazily-evaluated, auto-waiting reference to one or more elements. **Preferred** for interacting with elements. |
|
|
29
|
+
| **ElementHandle** | Static reference to a single DOM element. Useful for special interactions or JS execution. |
|
|
30
|
+
| **JSHandle** | Handle to any JavaScript object, not just DOM nodes. Returned by `evaluate_handle()`. |
|
|
31
|
+
|
|
8
32
|
Here are few few examples of the most typical page matches with the different selector types:
|
|
9
33
|
|
|
10
34
|
| **Element to Match** | **CSS** | **XPath** | **Playwright `get_by_*` Method** |
|
|
@@ -35,9 +59,12 @@ __email__ = "mdiefenb@opentext.com"
|
|
|
35
59
|
|
|
36
60
|
import logging
|
|
37
61
|
import os
|
|
62
|
+
import subprocess
|
|
38
63
|
import tempfile
|
|
39
64
|
import time
|
|
65
|
+
import traceback
|
|
40
66
|
from http import HTTPStatus
|
|
67
|
+
from types import TracebackType
|
|
41
68
|
|
|
42
69
|
default_logger = logging.getLogger("pyxecm.customizer.browser_automation")
|
|
43
70
|
|
|
@@ -48,7 +75,7 @@ try:
|
|
|
48
75
|
from playwright.sync_api import (
|
|
49
76
|
Browser,
|
|
50
77
|
BrowserContext,
|
|
51
|
-
|
|
78
|
+
Locator,
|
|
52
79
|
Page,
|
|
53
80
|
sync_playwright,
|
|
54
81
|
)
|
|
@@ -66,6 +93,7 @@ REQUEST_TIMEOUT = 30
|
|
|
66
93
|
REQUEST_RETRY_DELAY = 2
|
|
67
94
|
REQUEST_MAX_RETRIES = 3
|
|
68
95
|
|
|
96
|
+
|
|
69
97
|
class BrowserAutomation:
|
|
70
98
|
"""Class to automate settings via a browser interface."""
|
|
71
99
|
|
|
@@ -82,6 +110,7 @@ class BrowserAutomation:
|
|
|
82
110
|
headless: bool = True,
|
|
83
111
|
logger: logging.Logger = default_logger,
|
|
84
112
|
wait_until: str | None = None,
|
|
113
|
+
browser: str | None = None,
|
|
85
114
|
) -> None:
|
|
86
115
|
"""Initialize the object.
|
|
87
116
|
|
|
@@ -106,12 +135,15 @@ class BrowserAutomation:
|
|
|
106
135
|
If True, the browser will be started in headless mode. Defaults to True.
|
|
107
136
|
wait_until (str | None, optional):
|
|
108
137
|
Wait until a certain condition. Options are:
|
|
109
|
-
* "
|
|
110
|
-
* "
|
|
111
|
-
* "
|
|
138
|
+
* "commit" - does not wait at all - commit the request and continue
|
|
139
|
+
* "load" - waits for the load event (after all resources like images/scripts load)
|
|
140
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
141
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
112
142
|
but subresources may still load).
|
|
113
143
|
logger (logging.Logger, optional):
|
|
114
144
|
The logging object to use for all log messages. Defaults to default_logger.
|
|
145
|
+
browser (str | None, optional):
|
|
146
|
+
The browser to use. Defaults to None, which takes the global default or from the ENV "BROWSER".
|
|
115
147
|
|
|
116
148
|
"""
|
|
117
149
|
|
|
@@ -134,9 +166,12 @@ class BrowserAutomation:
|
|
|
134
166
|
self.logged_in = False
|
|
135
167
|
self.download_directory = download_directory
|
|
136
168
|
|
|
169
|
+
# Screenshot configurations:
|
|
137
170
|
self.take_screenshots = take_screenshots
|
|
138
171
|
self.screenshot_names = automation_name
|
|
139
172
|
self.screenshot_counter = 1
|
|
173
|
+
self.screenshot_full_page = True
|
|
174
|
+
|
|
140
175
|
self.wait_until = wait_until if wait_until else DEFAULT_WAIT_UNTIL_STRATEGY
|
|
141
176
|
|
|
142
177
|
self.screenshot_directory = os.path.join(
|
|
@@ -145,15 +180,110 @@ class BrowserAutomation:
|
|
|
145
180
|
automation_name,
|
|
146
181
|
"screenshots",
|
|
147
182
|
)
|
|
183
|
+
self.logger.debug("Creating Screenshot directory... -> %s", self.screenshot_directory)
|
|
148
184
|
if self.take_screenshots and not os.path.exists(self.screenshot_directory):
|
|
149
185
|
os.makedirs(self.screenshot_directory)
|
|
150
186
|
|
|
187
|
+
self.logger.debug("Creating Playwright instance...")
|
|
151
188
|
self.playwright = sync_playwright().start()
|
|
152
|
-
|
|
189
|
+
|
|
190
|
+
proxy = None
|
|
191
|
+
if os.getenv("HTTP_PROXY"):
|
|
192
|
+
proxy = {
|
|
193
|
+
"server": os.getenv("HTTP_PROXY"),
|
|
194
|
+
}
|
|
195
|
+
self.logger.info("Using HTTP proxy -> %s", os.getenv("HTTP_PROXY"))
|
|
196
|
+
|
|
197
|
+
browser = browser or os.getenv("BROWSER", "webkit")
|
|
198
|
+
self.logger.info("Using Browser -> '%s'...", browser)
|
|
199
|
+
|
|
200
|
+
match browser:
|
|
201
|
+
case "chromium":
|
|
202
|
+
try:
|
|
203
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
204
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
self.install_browser(browser=browser)
|
|
208
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
209
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
case "chrome":
|
|
213
|
+
try:
|
|
214
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
215
|
+
channel="chrome", headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
216
|
+
)
|
|
217
|
+
except Exception:
|
|
218
|
+
self.install_browser(browser=browser)
|
|
219
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
220
|
+
channel="chrome", headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
case "msedge":
|
|
224
|
+
try:
|
|
225
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
226
|
+
channel="msedge", headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
227
|
+
)
|
|
228
|
+
except Exception:
|
|
229
|
+
self.install_browser(browser=browser)
|
|
230
|
+
self.browser: Browser = self.playwright.chromium.launch(
|
|
231
|
+
channel="msedge", headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
case "webkit":
|
|
235
|
+
try:
|
|
236
|
+
self.browser: Browser = self.playwright.webkit.launch(
|
|
237
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
238
|
+
)
|
|
239
|
+
except Exception:
|
|
240
|
+
self.install_browser(browser=browser)
|
|
241
|
+
self.browser: Browser = self.playwright.webkit.launch(
|
|
242
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
case "firefox":
|
|
246
|
+
try:
|
|
247
|
+
self.browser: Browser = self.playwright.firefox.launch(
|
|
248
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
249
|
+
)
|
|
250
|
+
except Exception:
|
|
251
|
+
self.install_browser(browser=browser)
|
|
252
|
+
self.browser: Browser = self.playwright.firefox.launch(
|
|
253
|
+
headless=headless, slow_mo=100 if not headless else None, proxy=proxy
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.logger.info("Creating Browser Context...")
|
|
153
257
|
self.context: BrowserContext = self.browser.new_context(
|
|
154
258
|
accept_downloads=True,
|
|
155
259
|
)
|
|
260
|
+
|
|
261
|
+
self.logger.info("Creating Page...")
|
|
156
262
|
self.page: Page = self.context.new_page()
|
|
263
|
+
self.main_page = self.page
|
|
264
|
+
self.logger.info("Browser Automation initialized.")
|
|
265
|
+
|
|
266
|
+
# end method definition
|
|
267
|
+
|
|
268
|
+
def install_browser(self, browser: str) -> bool:
|
|
269
|
+
"""Check if browser is already installed if not install it."""
|
|
270
|
+
|
|
271
|
+
self.logger.info("Installing Browser -> '%s'...", browser)
|
|
272
|
+
process = subprocess.Popen(
|
|
273
|
+
["playwright", "install", browser], # noqa: S607
|
|
274
|
+
stdout=subprocess.PIPE,
|
|
275
|
+
stderr=subprocess.PIPE,
|
|
276
|
+
shell=False,
|
|
277
|
+
)
|
|
278
|
+
output, error = process.communicate()
|
|
279
|
+
if process.returncode == 0:
|
|
280
|
+
self.logger.info("Installation completed successfullly.")
|
|
281
|
+
self.logger.debug(output.decode())
|
|
282
|
+
else:
|
|
283
|
+
self.logger.error("Installation failed with -> %s", error.decode())
|
|
284
|
+
self.logger.error(output.decode())
|
|
285
|
+
|
|
286
|
+
return True
|
|
157
287
|
|
|
158
288
|
# end method definition
|
|
159
289
|
|
|
@@ -166,7 +296,7 @@ class BrowserAutomation:
|
|
|
166
296
|
|
|
167
297
|
"""
|
|
168
298
|
|
|
169
|
-
screenshot_file = "{}/{}-{}.png".format(
|
|
299
|
+
screenshot_file = "{}/{}-{:02d}.png".format(
|
|
170
300
|
self.screenshot_directory,
|
|
171
301
|
self.screenshot_names,
|
|
172
302
|
self.screenshot_counter,
|
|
@@ -174,7 +304,7 @@ class BrowserAutomation:
|
|
|
174
304
|
self.logger.debug("Save browser screenshot to -> %s", screenshot_file)
|
|
175
305
|
|
|
176
306
|
try:
|
|
177
|
-
self.page.screenshot(path=screenshot_file)
|
|
307
|
+
self.page.screenshot(path=screenshot_file, full_page=self.screenshot_full_page)
|
|
178
308
|
self.screenshot_counter += 1
|
|
179
309
|
except Exception as e:
|
|
180
310
|
self.logger.error("Failed to take screenshot; error -> %s", e)
|
|
@@ -192,12 +322,13 @@ class BrowserAutomation:
|
|
|
192
322
|
URL to load. If empty just the base URL will be used.
|
|
193
323
|
wait_until (str | None, optional):
|
|
194
324
|
Wait until a certain condition. Options are:
|
|
195
|
-
* "
|
|
325
|
+
* "commit" - does not wait at all - commit the request and continue
|
|
326
|
+
* "load" - waits for the load event (after all resources like images/scripts load)
|
|
196
327
|
This is the safest strategy for pages that keep loading content in the background
|
|
197
328
|
like Salesforce.
|
|
198
|
-
* "networkidle" -
|
|
329
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
199
330
|
This seems to be the safest one for OpenText Content Server.
|
|
200
|
-
* "domcontentloaded" -
|
|
331
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
201
332
|
but subresources may still load).
|
|
202
333
|
|
|
203
334
|
Returns:
|
|
@@ -214,7 +345,7 @@ class BrowserAutomation:
|
|
|
214
345
|
page_url = self.base_url + url
|
|
215
346
|
|
|
216
347
|
try:
|
|
217
|
-
self.logger.debug("Load page -> %s", page_url)
|
|
348
|
+
self.logger.debug("Load page -> %s (wait until -> '%s')", page_url, wait_until)
|
|
218
349
|
|
|
219
350
|
# The Playwright Response object is different from the requests.response object!
|
|
220
351
|
response = self.page.goto(page_url, wait_until=wait_until)
|
|
@@ -258,38 +389,50 @@ class BrowserAutomation:
|
|
|
258
389
|
Args:
|
|
259
390
|
wait_until (str | None, optional):
|
|
260
391
|
Wait until a certain condition. Options are:
|
|
261
|
-
* "
|
|
392
|
+
* "commit" - does not wait at all - commit the request and continue
|
|
393
|
+
* "load" - waits for the load event (after all resources like images/scripts load)
|
|
262
394
|
This is the safest strategy for pages that keep loading content in the background
|
|
263
395
|
like Salesforce.
|
|
264
|
-
* "networkidle" -
|
|
396
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
265
397
|
This seems to be the safest one for OpenText Content Server.
|
|
266
|
-
* "domcontentloaded" -
|
|
398
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
267
399
|
but subresources may still load).
|
|
268
400
|
|
|
269
401
|
Returns:
|
|
270
402
|
str:
|
|
271
|
-
The title of the browser
|
|
403
|
+
The title of the browser page.
|
|
272
404
|
|
|
273
405
|
"""
|
|
274
406
|
|
|
275
|
-
for
|
|
407
|
+
for attempt in range(REQUEST_MAX_RETRIES):
|
|
276
408
|
try:
|
|
277
|
-
|
|
409
|
+
if wait_until:
|
|
410
|
+
self.page.wait_for_load_state(state=wait_until, timeout=REQUEST_TIMEOUT)
|
|
411
|
+
title = self.page.title()
|
|
412
|
+
if title:
|
|
413
|
+
return title
|
|
414
|
+
time.sleep(REQUEST_RETRY_DELAY)
|
|
415
|
+
self.logger.info("Retry attempt %d/%d", attempt + 1, REQUEST_MAX_RETRIES)
|
|
278
416
|
except Exception as e:
|
|
279
417
|
if "Execution context was destroyed" in str(e):
|
|
418
|
+
self.logger.info(
|
|
419
|
+
"Execution context was destroyed, retrying after %s seconds...", REQUEST_RETRY_DELAY
|
|
420
|
+
)
|
|
280
421
|
time.sleep(REQUEST_RETRY_DELAY)
|
|
281
|
-
self.
|
|
282
|
-
|
|
283
|
-
|
|
422
|
+
self.logger.info("Retry attempt %d/%d", attempt + 1, REQUEST_MAX_RETRIES)
|
|
423
|
+
continue
|
|
424
|
+
self.logger.error("Could not get page title; error -> %s", str(e))
|
|
425
|
+
break
|
|
284
426
|
|
|
285
427
|
return None
|
|
428
|
+
|
|
286
429
|
# end method definition
|
|
287
430
|
|
|
288
|
-
def scroll_to_element(self, element:
|
|
431
|
+
def scroll_to_element(self, element: Locator) -> None:
|
|
289
432
|
"""Scroll an element into view to make it clickable.
|
|
290
433
|
|
|
291
434
|
Args:
|
|
292
|
-
element (
|
|
435
|
+
element (Locator):
|
|
293
436
|
Web element that has been identified before.
|
|
294
437
|
|
|
295
438
|
"""
|
|
@@ -305,41 +448,28 @@ class BrowserAutomation:
|
|
|
305
448
|
|
|
306
449
|
# end method definition
|
|
307
450
|
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
selector: str,
|
|
311
|
-
selector_type: str = "id",
|
|
312
|
-
role_type: str | None = None,
|
|
313
|
-
show_error: bool = True,
|
|
314
|
-
) -> ElementHandle | None:
|
|
315
|
-
"""Find a page element.
|
|
451
|
+
def get_locator(self, selector: str, selector_type: str, role_type: str | None = None) -> Locator | None:
|
|
452
|
+
"""Determine the locator for the given selector type and (optional) role type.
|
|
316
453
|
|
|
317
454
|
Args:
|
|
318
455
|
selector (str):
|
|
319
|
-
The
|
|
320
|
-
selector_type (str
|
|
456
|
+
The selector to find the element on the page.
|
|
457
|
+
selector_type (str):
|
|
321
458
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
322
459
|
"label", "placeholder", "alt".
|
|
460
|
+
When using css, the selector becomes a raw CSS selector, and you can skip attribute
|
|
461
|
+
and value filtering entirely if your selector already narrows it down.
|
|
462
|
+
Examples for CSS:
|
|
463
|
+
* selector="img" - find all img tags (images)
|
|
464
|
+
* selector="img[title]" - find all img tags (images) that have a title attribute - independent of its value
|
|
465
|
+
* selector="img[title*='Microsoft Teams']" - find all images with a title that contains "Microsoft Teams"
|
|
466
|
+
* selector=".toolbar button" - find all buttons inside a .toolbar class
|
|
323
467
|
role_type (str | None, optional):
|
|
324
468
|
ARIA role when using selector_type="role", e.g., "button", "textbox".
|
|
325
469
|
If irrelevant then None should be passed for role_type.
|
|
326
|
-
show_error (bool, optional):
|
|
327
|
-
Show an error if not found or not visible.
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
ElementHandle:
|
|
331
|
-
The web element or None in case an error occured.
|
|
332
470
|
|
|
333
471
|
"""
|
|
334
472
|
|
|
335
|
-
locator = None
|
|
336
|
-
failure_message = "Cannot find page element with selector -> '{}' ({}){}".format(
|
|
337
|
-
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
338
|
-
)
|
|
339
|
-
success_message = "Found page element with selector -> '{}' ('{}'){}".format(
|
|
340
|
-
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
341
|
-
)
|
|
342
|
-
|
|
343
473
|
try:
|
|
344
474
|
match selector_type:
|
|
345
475
|
case "id":
|
|
@@ -353,7 +483,8 @@ class BrowserAutomation:
|
|
|
353
483
|
case "css":
|
|
354
484
|
locator = self.page.locator(selector)
|
|
355
485
|
case "text":
|
|
356
|
-
locator = self.page.get_by_text(selector)
|
|
486
|
+
# locator = self.page.get_by_text(selector)
|
|
487
|
+
locator = self.page.locator("text={}".format(selector))
|
|
357
488
|
case "title":
|
|
358
489
|
locator = self.page.get_by_title(selector)
|
|
359
490
|
case "label":
|
|
@@ -371,23 +502,83 @@ class BrowserAutomation:
|
|
|
371
502
|
self.logger.error("Unsupported selector type -> '%s'", selector_type)
|
|
372
503
|
return None
|
|
373
504
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
505
|
+
except PlaywrightError as e:
|
|
506
|
+
self.logger.error("Failure to determine page locator; error -> %s", str(e))
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
return locator
|
|
510
|
+
|
|
511
|
+
# end method definition
|
|
512
|
+
|
|
513
|
+
def find_elem(
|
|
514
|
+
self,
|
|
515
|
+
selector: str,
|
|
516
|
+
selector_type: str = "id",
|
|
517
|
+
role_type: str | None = None,
|
|
518
|
+
wait_state: str = "visible",
|
|
519
|
+
show_error: bool = True,
|
|
520
|
+
) -> Locator | None:
|
|
521
|
+
"""Find a page element.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
selector (str):
|
|
525
|
+
The name of the page element or accessible name (for role).
|
|
526
|
+
selector_type (str, optional):
|
|
527
|
+
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
528
|
+
"label", "placeholder", "alt".
|
|
529
|
+
role_type (str | None, optional):
|
|
530
|
+
ARIA role when using selector_type="role", e.g., "button", "textbox".
|
|
531
|
+
If irrelevant then None should be passed for role_type.
|
|
532
|
+
wait_state (str, optional):
|
|
533
|
+
Defines if we wait for attached (element is part of DOM) or
|
|
534
|
+
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
535
|
+
show_error (bool, optional):
|
|
536
|
+
Show an error if not found or not visible.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Locator:
|
|
540
|
+
The web element or None in case an error occured.
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
failure_message = "Cannot find page element with selector -> '{}' ({}){}".format(
|
|
545
|
+
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
546
|
+
)
|
|
547
|
+
success_message = "Found page element with selector -> '{}' ('{}'){}".format(
|
|
548
|
+
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Determine the locator for the element:
|
|
552
|
+
locator = self.get_locator(selector=selector, selector_type=selector_type, role_type=role_type)
|
|
553
|
+
if not locator:
|
|
554
|
+
if show_error:
|
|
555
|
+
self.logger.error(failure_message)
|
|
380
556
|
else:
|
|
381
|
-
self.logger.
|
|
557
|
+
self.logger.warning(failure_message)
|
|
558
|
+
return None
|
|
382
559
|
|
|
383
|
-
|
|
560
|
+
# Wait for the element to be visible - don't use logic like
|
|
561
|
+
# locator.count() as this does not wait but fail immideately if elements
|
|
562
|
+
# are not yet loaded:
|
|
563
|
+
try:
|
|
564
|
+
self.logger.info(
|
|
565
|
+
"Wait for locator to find element with selector -> '%s' (%s%s) and state -> '%s'...",
|
|
566
|
+
selector,
|
|
567
|
+
"selector type -> '{}'".format(selector_type),
|
|
568
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
569
|
+
wait_state,
|
|
570
|
+
)
|
|
571
|
+
locator.wait_for(state=wait_state)
|
|
572
|
+
except PlaywrightError:
|
|
384
573
|
if show_error:
|
|
385
|
-
self.logger.error("%s
|
|
574
|
+
self.logger.error("%s (timeout)", failure_message)
|
|
386
575
|
else:
|
|
387
|
-
self.logger.warning("%s
|
|
576
|
+
self.logger.warning("%s (timeout)", failure_message)
|
|
388
577
|
return None
|
|
578
|
+
else:
|
|
579
|
+
self.logger.debug(success_message)
|
|
389
580
|
|
|
390
|
-
return
|
|
581
|
+
return locator
|
|
391
582
|
|
|
392
583
|
# end method definition
|
|
393
584
|
|
|
@@ -399,7 +590,10 @@ class BrowserAutomation:
|
|
|
399
590
|
scroll_to_element: bool = True,
|
|
400
591
|
desired_checkbox_state: bool | None = None,
|
|
401
592
|
is_navigation_trigger: bool = False,
|
|
593
|
+
is_popup_trigger: bool = False,
|
|
594
|
+
is_page_close_trigger: bool = False,
|
|
402
595
|
wait_until: str | None = None,
|
|
596
|
+
wait_time: float = 0.0,
|
|
403
597
|
show_error: bool = True,
|
|
404
598
|
) -> bool:
|
|
405
599
|
"""Find a page element and click it.
|
|
@@ -420,15 +614,22 @@ class BrowserAutomation:
|
|
|
420
614
|
If None then click it in any case.
|
|
421
615
|
is_navigation_trigger (bool, optional):
|
|
422
616
|
Is the click causing a navigation. Default is False.
|
|
617
|
+
is_popup_trigger (bool, optional):
|
|
618
|
+
Is the click causing a new browser window to open?
|
|
619
|
+
is_page_close_trigger (bool, optional):
|
|
620
|
+
Is the click causing the page to close?
|
|
423
621
|
wait_until (str | None, optional):
|
|
424
622
|
Wait until a certain condition. Options are:
|
|
425
|
-
* "
|
|
623
|
+
* "commit" - does not wait at all - commit the request and continue
|
|
624
|
+
* "load" - waits for the load event (after all resources like images/scripts load)
|
|
426
625
|
This is the safest strategy for pages that keep loading content in the background
|
|
427
626
|
like Salesforce.
|
|
428
|
-
* "networkidle" -
|
|
627
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
429
628
|
This seems to be the safest one for OpenText Content Server.
|
|
430
|
-
* "domcontentloaded" -
|
|
629
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
431
630
|
but subresources may still load).
|
|
631
|
+
wait_time (float):
|
|
632
|
+
Time in seconds to wait for elements to appear.
|
|
432
633
|
show_error (bool, optional):
|
|
433
634
|
Show an error if the element is not found or not clickable.
|
|
434
635
|
|
|
@@ -439,11 +640,19 @@ class BrowserAutomation:
|
|
|
439
640
|
|
|
440
641
|
"""
|
|
441
642
|
|
|
643
|
+
success = True # Final return value
|
|
644
|
+
|
|
442
645
|
# If no specific wait until strategy is provided in the
|
|
443
646
|
# parameter, we take the one from the browser automation class:
|
|
444
647
|
if wait_until is None:
|
|
445
648
|
wait_until = self.wait_until
|
|
446
649
|
|
|
650
|
+
# Some operations that are done server-side and dynamically update
|
|
651
|
+
# the page may require a waiting time:
|
|
652
|
+
if wait_time > 0.0:
|
|
653
|
+
self.logger.info("Wait for %d milliseconds before clicking...", wait_time * 1000)
|
|
654
|
+
self.page.wait_for_timeout(wait_time * 1000)
|
|
655
|
+
|
|
447
656
|
if not selector:
|
|
448
657
|
failure_message = "Missing element selector! Cannot find page element!"
|
|
449
658
|
if show_error:
|
|
@@ -462,74 +671,74 @@ class BrowserAutomation:
|
|
|
462
671
|
if scroll_to_element:
|
|
463
672
|
self.scroll_to_element(elem)
|
|
464
673
|
|
|
465
|
-
# Handle checkboxes
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
674
|
+
# Handle checkboxes if requested:
|
|
675
|
+
if desired_checkbox_state is not None and elem.get_attribute("type") == "checkbox":
|
|
676
|
+
# Let Playwright handle checkbox state:
|
|
677
|
+
elem.set_checked(desired_checkbox_state)
|
|
678
|
+
self.logger.debug("Set checkbox -> '%s' to value -> %s.", selector, desired_checkbox_state)
|
|
679
|
+
# Handle non-checkboxes:
|
|
680
|
+
else:
|
|
681
|
+
# Will this click trigger a naviagation?
|
|
682
|
+
if is_navigation_trigger:
|
|
683
|
+
self.logger.info(
|
|
684
|
+
"Clicking on navigation-triggering element -> '%s' (%s%s) and wait until -> '%s'...",
|
|
685
|
+
selector,
|
|
686
|
+
"selector type -> '{}'".format(selector_type),
|
|
687
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
688
|
+
wait_until,
|
|
474
689
|
)
|
|
475
|
-
return True # No need to click
|
|
476
|
-
else:
|
|
477
|
-
self.logger.debug("Checkbox -> '%s' has state mismatch. Clicking to change state.", selector)
|
|
478
|
-
|
|
479
|
-
if is_navigation_trigger:
|
|
480
|
-
self.logger.info("Clicking on navigation-triggering element -> '%s'", selector)
|
|
481
|
-
try:
|
|
482
690
|
with self.page.expect_navigation(wait_until=wait_until):
|
|
483
691
|
elem.click()
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
692
|
+
# Will this click trigger a a new popup window?
|
|
693
|
+
elif is_popup_trigger:
|
|
694
|
+
with self.page.expect_popup() as popup_info:
|
|
695
|
+
elem.click()
|
|
696
|
+
if not popup_info or not popup_info.value:
|
|
697
|
+
self.logger.info("Popup window did not open as expected!")
|
|
698
|
+
success = False
|
|
699
|
+
else:
|
|
700
|
+
self.page = popup_info.value
|
|
701
|
+
self.logger.info("Move browser automation to popup window -> %s...", self.page.url)
|
|
702
|
+
else:
|
|
703
|
+
self.logger.info(
|
|
704
|
+
"Clicking on non-navigating element -> '%s' (%s%s)...",
|
|
487
705
|
selector,
|
|
488
|
-
|
|
706
|
+
"selector type -> '{}'".format(selector_type),
|
|
707
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
489
708
|
)
|
|
490
|
-
return False
|
|
491
|
-
else:
|
|
492
|
-
self.logger.info("Clicking on non-navigating element -> '%s'", selector)
|
|
493
|
-
try:
|
|
494
709
|
elem.click()
|
|
495
710
|
time.sleep(1)
|
|
496
|
-
|
|
497
|
-
self.logger.error("Click failed -> %s", str(e))
|
|
498
|
-
return False
|
|
499
|
-
|
|
500
|
-
if is_checkbox and desired_checkbox_state is not None:
|
|
501
|
-
elem = self.find_elem(selector=selector, selector_type=selector_type, show_error=show_error)
|
|
502
|
-
if elem:
|
|
503
|
-
checkbox_state = elem.is_checked()
|
|
504
|
-
|
|
505
|
-
if checkbox_state is not None:
|
|
506
|
-
if checkbox_state == desired_checkbox_state:
|
|
711
|
+
if success:
|
|
507
712
|
self.logger.debug(
|
|
508
|
-
"Successfully clicked
|
|
713
|
+
"Successfully clicked element -> '%s' (%s%s)",
|
|
509
714
|
selector,
|
|
510
|
-
|
|
715
|
+
"selector type -> '{}'".format(selector_type),
|
|
716
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
511
717
|
)
|
|
512
|
-
else:
|
|
513
|
-
self.logger.error(
|
|
514
|
-
"Failed to flip checkbox element -> '%s' to desired state. It's state is still -> %s and not -> %s",
|
|
515
|
-
selector,
|
|
516
|
-
checkbox_state,
|
|
517
|
-
desired_checkbox_state,
|
|
518
|
-
)
|
|
519
|
-
else:
|
|
520
|
-
self.logger.debug("Successfully clicked element -> '%s'", selector)
|
|
521
|
-
|
|
522
|
-
if self.take_screenshots:
|
|
523
|
-
self.take_screenshot()
|
|
524
718
|
|
|
525
719
|
except PlaywrightError as e:
|
|
526
720
|
if show_error:
|
|
527
|
-
self.logger.error(
|
|
721
|
+
self.logger.error(
|
|
722
|
+
"Cannot click page element -> '%s' (%s); error -> %s", selector, selector_type, str(e)
|
|
723
|
+
)
|
|
528
724
|
else:
|
|
529
|
-
self.logger.warning(
|
|
530
|
-
|
|
725
|
+
self.logger.warning(
|
|
726
|
+
"Cannot click page element -> '%s' (%s); warning -> %s", selector, selector_type, str(e)
|
|
727
|
+
)
|
|
728
|
+
success = not show_error
|
|
531
729
|
|
|
532
|
-
|
|
730
|
+
if is_page_close_trigger:
|
|
731
|
+
if self.page == self.main_page:
|
|
732
|
+
self.logger.error("Unexpected try to close main page! Popup page not active! This is not supported!")
|
|
733
|
+
success = False
|
|
734
|
+
else:
|
|
735
|
+
self.page = self.main_page
|
|
736
|
+
self.logger.info("Move browser automation back to main window -> %s...", self.page.url)
|
|
737
|
+
|
|
738
|
+
if self.take_screenshots:
|
|
739
|
+
self.take_screenshot()
|
|
740
|
+
|
|
741
|
+
return success
|
|
533
742
|
|
|
534
743
|
# end method definition
|
|
535
744
|
|
|
@@ -566,6 +775,8 @@ class BrowserAutomation:
|
|
|
566
775
|
|
|
567
776
|
"""
|
|
568
777
|
|
|
778
|
+
success = False # Final return value
|
|
779
|
+
|
|
569
780
|
elem = self.find_elem(selector=selector, selector_type=selector_type, role_type=role_type, show_error=True)
|
|
570
781
|
if not elem:
|
|
571
782
|
return False
|
|
@@ -580,12 +791,14 @@ class BrowserAutomation:
|
|
|
580
791
|
else:
|
|
581
792
|
self.logger.warning(message)
|
|
582
793
|
|
|
794
|
+
if self.take_screenshots:
|
|
795
|
+
self.take_screenshot()
|
|
796
|
+
|
|
583
797
|
return False
|
|
584
798
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
self.logger.debug("Set element -> %s to value -> <sensitive>...", selector)
|
|
799
|
+
self.logger.info(
|
|
800
|
+
"Set element -> '%s' to value -> '%s'...", selector, value if not is_sensitive else "<sensitive>"
|
|
801
|
+
)
|
|
589
802
|
|
|
590
803
|
try:
|
|
591
804
|
# HTML '<select>' can only be identified based on its tag name:
|
|
@@ -594,27 +807,38 @@ class BrowserAutomation:
|
|
|
594
807
|
input_type = elem.get_attribute("type")
|
|
595
808
|
|
|
596
809
|
if tag_name == "select":
|
|
597
|
-
options = elem.
|
|
598
|
-
|
|
810
|
+
options = elem.locator("option")
|
|
811
|
+
options_count = options.count()
|
|
812
|
+
option_values = [options.nth(i).inner_text().strip().replace("\n", "") for i in range(options_count)]
|
|
813
|
+
|
|
599
814
|
if value not in option_values:
|
|
600
815
|
self.logger.warning(
|
|
601
|
-
"Provided value -> '%s' not in available drop-down options -> %s. Cannot set it!",
|
|
816
|
+
"Provided value -> '%s' is not in available drop-down options -> %s. Cannot set it!",
|
|
602
817
|
value,
|
|
603
818
|
option_values,
|
|
604
819
|
)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
820
|
+
else:
|
|
821
|
+
# We set the value over the (visible) label:
|
|
822
|
+
elem.select_option(label=value)
|
|
823
|
+
success = True
|
|
608
824
|
elif tag_name == "input" and input_type == "checkbox":
|
|
609
825
|
# Handle checkbox
|
|
610
826
|
if not isinstance(value, bool):
|
|
611
827
|
self.logger.error("Checkbox value must be a boolean!")
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
828
|
+
else:
|
|
829
|
+
retry = 0
|
|
830
|
+
while elem.is_checked() != value and retry < 5:
|
|
831
|
+
try:
|
|
832
|
+
elem.set_checked(checked=value)
|
|
833
|
+
except Exception:
|
|
834
|
+
self.logger.warning("Cannot set checkbox to value -> '%s'. (retry %s).", value, retry)
|
|
835
|
+
finally:
|
|
836
|
+
retry += 1
|
|
837
|
+
|
|
838
|
+
success = retry < 5 # True is less than 5 retries were needed
|
|
616
839
|
else:
|
|
617
840
|
elem.fill(value)
|
|
841
|
+
success = True
|
|
618
842
|
except PlaywrightError as e:
|
|
619
843
|
message = "Cannot set page element selected by -> '{}' ({}) to value -> '{}'; error -> {}".format(
|
|
620
844
|
selector, selector_type, value, str(e)
|
|
@@ -623,12 +847,12 @@ class BrowserAutomation:
|
|
|
623
847
|
self.logger.error(message)
|
|
624
848
|
else:
|
|
625
849
|
self.logger.warning(message)
|
|
626
|
-
|
|
850
|
+
success = not show_error
|
|
627
851
|
|
|
628
852
|
if self.take_screenshots:
|
|
629
853
|
self.take_screenshot()
|
|
630
854
|
|
|
631
|
-
return
|
|
855
|
+
return success
|
|
632
856
|
|
|
633
857
|
# end method definition
|
|
634
858
|
|
|
@@ -690,13 +914,14 @@ class BrowserAutomation:
|
|
|
690
914
|
substring: bool = True,
|
|
691
915
|
min_count: int = 1,
|
|
692
916
|
wait_time: float = 0.0,
|
|
917
|
+
wait_state: str = "visible",
|
|
693
918
|
show_error: bool = True,
|
|
694
919
|
) -> tuple[bool | None, int]:
|
|
695
920
|
"""Check if (multiple) elements with defined attributes exist on page and return the number.
|
|
696
921
|
|
|
697
922
|
Args:
|
|
698
923
|
selector (str):
|
|
699
|
-
|
|
924
|
+
The selector to find the element on the page.
|
|
700
925
|
selector_type (str):
|
|
701
926
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
702
927
|
"label", "placeholder", "alt".
|
|
@@ -720,6 +945,9 @@ class BrowserAutomation:
|
|
|
720
945
|
Minimum number of required matches (# elements on page).
|
|
721
946
|
wait_time (float):
|
|
722
947
|
Time in seconds to wait for elements to appear.
|
|
948
|
+
wait_state (str, optional):
|
|
949
|
+
Defines if we wait for attached (element is part of DOM) or
|
|
950
|
+
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
723
951
|
show_error (bool):
|
|
724
952
|
Whether to log warnings/errors.
|
|
725
953
|
|
|
@@ -732,86 +960,126 @@ class BrowserAutomation:
|
|
|
732
960
|
|
|
733
961
|
"""
|
|
734
962
|
|
|
963
|
+
failure_message = "No matching page element found with selector -> '{}' ({}){}".format(
|
|
964
|
+
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Determine the locator for the elements:
|
|
968
|
+
locator = self.get_locator(selector=selector, selector_type=selector_type, role_type=role_type)
|
|
969
|
+
if not locator:
|
|
970
|
+
if show_error:
|
|
971
|
+
self.logger.error(
|
|
972
|
+
"Failed to check if elements -> '%s' (%s) exist! Locator is undefined.", selector, selector_type
|
|
973
|
+
)
|
|
974
|
+
return (None, 0)
|
|
975
|
+
|
|
976
|
+
self.logger.info(
|
|
977
|
+
"Check if at least %d element%s found by selector -> %s (%s%s)%s%s...",
|
|
978
|
+
min_count,
|
|
979
|
+
"s are" if min_count > 1 else " is",
|
|
980
|
+
selector,
|
|
981
|
+
"selector type -> '{}'".format(selector_type),
|
|
982
|
+
", role type -> {}".format(role_type) if role_type else "",
|
|
983
|
+
" with value -> '{}'".format(value) if value else "",
|
|
984
|
+
" in attribute -> '{}'".format(attribute) if attribute and value else "",
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
# Wait for the element to be visible - don't immediately use logic like
|
|
988
|
+
# locator.count() as this does not wait but then fail immideately
|
|
989
|
+
try:
|
|
990
|
+
self.logger.info(
|
|
991
|
+
"Wait for locator to find first matching element with selector -> '%s' (%s%s) and state -> '%s'...",
|
|
992
|
+
selector,
|
|
993
|
+
"selector type -> '{}'".format(selector_type),
|
|
994
|
+
", role type -> {}".format(role_type) if role_type else "",
|
|
995
|
+
wait_state,
|
|
996
|
+
)
|
|
997
|
+
self.logger.info("Locator count before waiting: %d", locator.count())
|
|
998
|
+
|
|
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
|
+
# IMPORTANT: We wait for the FIRST element. otherwise we get errors like
|
|
1004
|
+
# 'Locator.wait_for: Error: strict mode violation'.
|
|
1005
|
+
# IMPORTANT: if the first match does not comply to the
|
|
1006
|
+
# wait_state this will block and then timeout. Check your
|
|
1007
|
+
# selector to make sure it delivers a visible first element!
|
|
1008
|
+
locator.first.wait_for(state=wait_state)
|
|
1009
|
+
except PlaywrightError as e:
|
|
1010
|
+
# This is typically a timeout error indicating the element does not exist
|
|
1011
|
+
# in the defined timeout period.
|
|
1012
|
+
if show_error:
|
|
1013
|
+
self.logger.error("%s (timeout); error -> %s", failure_message, str(e))
|
|
1014
|
+
else:
|
|
1015
|
+
self.logger.warning("%s (timeout)", failure_message)
|
|
1016
|
+
return (None, 0)
|
|
1017
|
+
|
|
735
1018
|
# Some operations that are done server-side and dynamically update
|
|
736
|
-
# the page may require a waiting time:
|
|
1019
|
+
# the page with additional matching elements that may require a waiting time:
|
|
737
1020
|
if wait_time > 0.0:
|
|
738
|
-
self.logger.info("Wait
|
|
1021
|
+
self.logger.info("Wait additional %d milliseconds before checking...", wait_time * 1000)
|
|
739
1022
|
self.page.wait_for_timeout(wait_time * 1000)
|
|
740
1023
|
|
|
741
|
-
|
|
742
|
-
match selector_type:
|
|
743
|
-
case "id":
|
|
744
|
-
locator = self.page.locator("#{}".format(selector))
|
|
745
|
-
case "name":
|
|
746
|
-
locator = self.page.locator("[name='{}']".format(selector))
|
|
747
|
-
case "class_name":
|
|
748
|
-
locator = self.page.locator(".{}".format(selector))
|
|
749
|
-
case "xpath":
|
|
750
|
-
locator = self.page.locator("xpath={}".format(selector))
|
|
751
|
-
case "css":
|
|
752
|
-
locator = self.page.locator(selector)
|
|
753
|
-
case "text":
|
|
754
|
-
locator = self.page.get_by_text(selector)
|
|
755
|
-
case "title":
|
|
756
|
-
locator = self.page.get_by_title(selector)
|
|
757
|
-
case "label":
|
|
758
|
-
locator = self.page.get_by_label(selector)
|
|
759
|
-
case "placeholder":
|
|
760
|
-
locator = self.page.get_by_placeholder(selector)
|
|
761
|
-
case "alt":
|
|
762
|
-
locator = self.page.get_by_alt_text(selector)
|
|
763
|
-
case "role":
|
|
764
|
-
if not role_type:
|
|
765
|
-
self.logger.error("Role type must be specified when using find method 'role'!")
|
|
766
|
-
return (None, 0)
|
|
767
|
-
locator = self.page.get_by_role(role=role_type, name=selector)
|
|
768
|
-
case _:
|
|
769
|
-
self.logger.error("Unsupported selector type -> '%s'", selector_type)
|
|
770
|
-
return (None, 0)
|
|
1024
|
+
matching_elems = []
|
|
771
1025
|
|
|
772
|
-
|
|
1026
|
+
count = locator.count()
|
|
1027
|
+
if count == 0:
|
|
1028
|
+
if show_error:
|
|
1029
|
+
self.logger.error("No elements found using selector -> '%s' ('%s')", selector, selector_type)
|
|
1030
|
+
return (None, 0)
|
|
773
1031
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1032
|
+
self.logger.info(
|
|
1033
|
+
"Found %s elements matching selector -> '%s' (%s%s).",
|
|
1034
|
+
count,
|
|
1035
|
+
selector,
|
|
1036
|
+
"selector type -> '{}'".format(selector_type),
|
|
1037
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
1038
|
+
)
|
|
779
1039
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if
|
|
783
|
-
|
|
1040
|
+
if value:
|
|
1041
|
+
self.logger.info(
|
|
1042
|
+
"Checking if their %s %s -> '%s'...",
|
|
1043
|
+
"attribute -> '{}'".format(attribute) if attribute else "content",
|
|
1044
|
+
"has value" if not substring else "contains",
|
|
1045
|
+
value,
|
|
1046
|
+
)
|
|
784
1047
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1048
|
+
for i in range(count):
|
|
1049
|
+
elem = locator.nth(i)
|
|
1050
|
+
if not elem:
|
|
1051
|
+
continue
|
|
789
1052
|
|
|
790
|
-
|
|
791
|
-
|
|
1053
|
+
if value is None:
|
|
1054
|
+
# No filtering, accept all elements
|
|
1055
|
+
matching_elems.append(elem)
|
|
1056
|
+
continue
|
|
792
1057
|
|
|
793
|
-
|
|
794
|
-
|
|
1058
|
+
# Get attribute or text content
|
|
1059
|
+
attr_value = elem.get_attribute(attribute) if attribute else elem.text_content()
|
|
795
1060
|
|
|
796
|
-
|
|
797
|
-
|
|
1061
|
+
if not attr_value:
|
|
1062
|
+
continue
|
|
798
1063
|
|
|
799
|
-
|
|
1064
|
+
if (substring and value in attr_value) or (not substring and value == attr_value):
|
|
1065
|
+
matching_elems.append(elem)
|
|
800
1066
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1067
|
+
matching_elements_count = len(matching_elems)
|
|
1068
|
+
|
|
1069
|
+
success = True
|
|
1070
|
+
|
|
1071
|
+
if matching_elements_count < min_count:
|
|
1072
|
+
success = False
|
|
1073
|
+
if show_error:
|
|
1074
|
+
self.logger.error(
|
|
1075
|
+
"%s matching element%s found, expected at least %d",
|
|
804
1076
|
"Only {}".format(matching_elements_count) if matching_elems else "No",
|
|
1077
|
+
"s" if matching_elements_count > 1 else "",
|
|
805
1078
|
min_count,
|
|
806
1079
|
)
|
|
807
|
-
|
|
1080
|
+
self.logger.info("Found %d matching elements.", matching_elements_count)
|
|
808
1081
|
|
|
809
|
-
|
|
810
|
-
if show_error:
|
|
811
|
-
self.logger.error("Failed to check if elements -> '%s' exist; errors -> %s", selector, str(e))
|
|
812
|
-
return (None, 0)
|
|
813
|
-
|
|
814
|
-
return (True, matching_elements_count)
|
|
1082
|
+
return (success, matching_elements_count)
|
|
815
1083
|
|
|
816
1084
|
# end method definition
|
|
817
1085
|
|
|
@@ -837,12 +1105,13 @@ class BrowserAutomation:
|
|
|
837
1105
|
The URL to the login page. Defaults to "".
|
|
838
1106
|
wait_until (str | None, optional):
|
|
839
1107
|
Wait until a certain condition. Options are:
|
|
840
|
-
* "
|
|
1108
|
+
* "commit" - does not wait at all - commit the request and continue
|
|
1109
|
+
* "load" - waits for the load event (after all resources like images/scripts load)
|
|
841
1110
|
This is the safest strategy for pages that keep loading content in the background
|
|
842
1111
|
like Salesforce.
|
|
843
|
-
* "networkidle" -
|
|
1112
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
844
1113
|
This seems to be the safest one for OpenText Content Server.
|
|
845
|
-
* "domcontentloaded" -
|
|
1114
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
846
1115
|
but subresources may still load).
|
|
847
1116
|
selector_type (str, optional):
|
|
848
1117
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
@@ -878,16 +1147,17 @@ class BrowserAutomation:
|
|
|
878
1147
|
)
|
|
879
1148
|
return False
|
|
880
1149
|
|
|
1150
|
+
self.logger.info("Wait for -> '%s' to assure login is completed and target page is loaded.", wait_until)
|
|
881
1151
|
self.page.wait_for_load_state(wait_until)
|
|
882
1152
|
|
|
883
1153
|
title = self.get_title()
|
|
884
1154
|
if not title:
|
|
885
1155
|
self.logger.error(
|
|
886
|
-
"Cannot read page title after login - you may have the wrong 'wait until' strategy configured!",
|
|
1156
|
+
"Cannot read page title after login - you may have the wrong 'wait until' strategy configured! Strategy user -> '%s'.",
|
|
1157
|
+
wait_until,
|
|
887
1158
|
)
|
|
888
1159
|
return False
|
|
889
1160
|
|
|
890
|
-
|
|
891
1161
|
if "Verify" in title:
|
|
892
1162
|
self.logger.error("Site is asking for a Verification Token. You may need to whitelist your IP!")
|
|
893
1163
|
return False
|
|
@@ -895,6 +1165,7 @@ class BrowserAutomation:
|
|
|
895
1165
|
self.logger.error("Authentication failed. You may have given the wrong password!")
|
|
896
1166
|
return False
|
|
897
1167
|
|
|
1168
|
+
self.logger.info("Login completed successfully! Page title -> '%s'", title)
|
|
898
1169
|
self.logged_in = True
|
|
899
1170
|
|
|
900
1171
|
return True
|
|
@@ -923,10 +1194,47 @@ class BrowserAutomation:
|
|
|
923
1194
|
def end_session(self) -> None:
|
|
924
1195
|
"""End the browser session and close the browser."""
|
|
925
1196
|
|
|
926
|
-
self.logger.
|
|
1197
|
+
self.logger.info("Close Browser Page...")
|
|
1198
|
+
self.page.close()
|
|
1199
|
+
self.logger.info("Close Browser Context...")
|
|
927
1200
|
self.context.close()
|
|
1201
|
+
self.logger.info("Close Browser...")
|
|
928
1202
|
self.browser.close()
|
|
929
1203
|
self.logged_in = False
|
|
1204
|
+
self.logger.info("Stop Playwright instance...")
|
|
930
1205
|
self.playwright.stop()
|
|
1206
|
+
self.logger.info("Browser automation has ended.")
|
|
1207
|
+
|
|
1208
|
+
# end method definition
|
|
1209
|
+
|
|
1210
|
+
def __enter__(self) -> object:
|
|
1211
|
+
"""Enable use with 'with' statement."""
|
|
1212
|
+
|
|
1213
|
+
return self
|
|
1214
|
+
|
|
1215
|
+
# end method definition
|
|
1216
|
+
|
|
1217
|
+
def __exit__(
|
|
1218
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback_obj: TracebackType | None
|
|
1219
|
+
) -> None:
|
|
1220
|
+
"""Handle cleanup when exiting a context manager block.
|
|
1221
|
+
|
|
1222
|
+
Ensures all browser-related resources are released. If an unhandled exception
|
|
1223
|
+
occurs within the context block, it will be logged before cleanup.
|
|
1224
|
+
|
|
1225
|
+
Args:
|
|
1226
|
+
exc_type (type[BaseException] | None): The class of the raised exception, if any.
|
|
1227
|
+
exc_value (BaseException | None): The exception instance raised, if any.
|
|
1228
|
+
traceback_obj (TracebackType | None): The traceback object associated with the exception, if any.
|
|
1229
|
+
|
|
1230
|
+
"""
|
|
1231
|
+
|
|
1232
|
+
if exc_type is not None:
|
|
1233
|
+
self.logger.error(
|
|
1234
|
+
"Unhandled exception in browser automation context -> %s",
|
|
1235
|
+
"".join(traceback.format_exception(exc_type, exc_value, traceback_obj)),
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
self.end_session()
|
|
931
1239
|
|
|
932
1240
|
# end method definition
|