pyxecm 2.0.2__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 +506 -199
- 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 +495 -205
- pyxecm/otds.py +1 -0
- pyxecm/otkd.py +1369 -0
- pyxecm/otmm.py +190 -66
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.3.dist-info}/METADATA +2 -2
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.3.dist-info}/RECORD +28 -26
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.3.dist-info}/WHEEL +1 -1
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.3.dist-info}/licenses/LICENSE +0 -0
- {pyxecm-2.0.2.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
|
)
|
|
@@ -83,6 +110,7 @@ class BrowserAutomation:
|
|
|
83
110
|
headless: bool = True,
|
|
84
111
|
logger: logging.Logger = default_logger,
|
|
85
112
|
wait_until: str | None = None,
|
|
113
|
+
browser: str | None = None,
|
|
86
114
|
) -> None:
|
|
87
115
|
"""Initialize the object.
|
|
88
116
|
|
|
@@ -107,12 +135,15 @@ class BrowserAutomation:
|
|
|
107
135
|
If True, the browser will be started in headless mode. Defaults to True.
|
|
108
136
|
wait_until (str | None, optional):
|
|
109
137
|
Wait until a certain condition. Options are:
|
|
110
|
-
* "
|
|
111
|
-
* "
|
|
112
|
-
* "
|
|
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,
|
|
113
142
|
but subresources may still load).
|
|
114
143
|
logger (logging.Logger, optional):
|
|
115
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".
|
|
116
147
|
|
|
117
148
|
"""
|
|
118
149
|
|
|
@@ -135,9 +166,12 @@ class BrowserAutomation:
|
|
|
135
166
|
self.logged_in = False
|
|
136
167
|
self.download_directory = download_directory
|
|
137
168
|
|
|
169
|
+
# Screenshot configurations:
|
|
138
170
|
self.take_screenshots = take_screenshots
|
|
139
171
|
self.screenshot_names = automation_name
|
|
140
172
|
self.screenshot_counter = 1
|
|
173
|
+
self.screenshot_full_page = True
|
|
174
|
+
|
|
141
175
|
self.wait_until = wait_until if wait_until else DEFAULT_WAIT_UNTIL_STRATEGY
|
|
142
176
|
|
|
143
177
|
self.screenshot_directory = os.path.join(
|
|
@@ -146,15 +180,110 @@ class BrowserAutomation:
|
|
|
146
180
|
automation_name,
|
|
147
181
|
"screenshots",
|
|
148
182
|
)
|
|
183
|
+
self.logger.debug("Creating Screenshot directory... -> %s", self.screenshot_directory)
|
|
149
184
|
if self.take_screenshots and not os.path.exists(self.screenshot_directory):
|
|
150
185
|
os.makedirs(self.screenshot_directory)
|
|
151
186
|
|
|
187
|
+
self.logger.debug("Creating Playwright instance...")
|
|
152
188
|
self.playwright = sync_playwright().start()
|
|
153
|
-
|
|
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...")
|
|
154
257
|
self.context: BrowserContext = self.browser.new_context(
|
|
155
258
|
accept_downloads=True,
|
|
156
259
|
)
|
|
260
|
+
|
|
261
|
+
self.logger.info("Creating Page...")
|
|
157
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
|
|
158
287
|
|
|
159
288
|
# end method definition
|
|
160
289
|
|
|
@@ -167,7 +296,7 @@ class BrowserAutomation:
|
|
|
167
296
|
|
|
168
297
|
"""
|
|
169
298
|
|
|
170
|
-
screenshot_file = "{}/{}-{}.png".format(
|
|
299
|
+
screenshot_file = "{}/{}-{:02d}.png".format(
|
|
171
300
|
self.screenshot_directory,
|
|
172
301
|
self.screenshot_names,
|
|
173
302
|
self.screenshot_counter,
|
|
@@ -175,7 +304,7 @@ class BrowserAutomation:
|
|
|
175
304
|
self.logger.debug("Save browser screenshot to -> %s", screenshot_file)
|
|
176
305
|
|
|
177
306
|
try:
|
|
178
|
-
self.page.screenshot(path=screenshot_file)
|
|
307
|
+
self.page.screenshot(path=screenshot_file, full_page=self.screenshot_full_page)
|
|
179
308
|
self.screenshot_counter += 1
|
|
180
309
|
except Exception as e:
|
|
181
310
|
self.logger.error("Failed to take screenshot; error -> %s", e)
|
|
@@ -193,12 +322,13 @@ class BrowserAutomation:
|
|
|
193
322
|
URL to load. If empty just the base URL will be used.
|
|
194
323
|
wait_until (str | None, optional):
|
|
195
324
|
Wait until a certain condition. Options are:
|
|
196
|
-
* "
|
|
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)
|
|
197
327
|
This is the safest strategy for pages that keep loading content in the background
|
|
198
328
|
like Salesforce.
|
|
199
|
-
* "networkidle" -
|
|
329
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
200
330
|
This seems to be the safest one for OpenText Content Server.
|
|
201
|
-
* "domcontentloaded" -
|
|
331
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
202
332
|
but subresources may still load).
|
|
203
333
|
|
|
204
334
|
Returns:
|
|
@@ -215,7 +345,7 @@ class BrowserAutomation:
|
|
|
215
345
|
page_url = self.base_url + url
|
|
216
346
|
|
|
217
347
|
try:
|
|
218
|
-
self.logger.debug("Load page -> %s", page_url)
|
|
348
|
+
self.logger.debug("Load page -> %s (wait until -> '%s')", page_url, wait_until)
|
|
219
349
|
|
|
220
350
|
# The Playwright Response object is different from the requests.response object!
|
|
221
351
|
response = self.page.goto(page_url, wait_until=wait_until)
|
|
@@ -259,39 +389,50 @@ class BrowserAutomation:
|
|
|
259
389
|
Args:
|
|
260
390
|
wait_until (str | None, optional):
|
|
261
391
|
Wait until a certain condition. Options are:
|
|
262
|
-
* "
|
|
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)
|
|
263
394
|
This is the safest strategy for pages that keep loading content in the background
|
|
264
395
|
like Salesforce.
|
|
265
|
-
* "networkidle" -
|
|
396
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
266
397
|
This seems to be the safest one for OpenText Content Server.
|
|
267
|
-
* "domcontentloaded" -
|
|
398
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
268
399
|
but subresources may still load).
|
|
269
400
|
|
|
270
401
|
Returns:
|
|
271
402
|
str:
|
|
272
|
-
The title of the browser
|
|
403
|
+
The title of the browser page.
|
|
273
404
|
|
|
274
405
|
"""
|
|
275
406
|
|
|
276
|
-
for
|
|
407
|
+
for attempt in range(REQUEST_MAX_RETRIES):
|
|
277
408
|
try:
|
|
278
|
-
|
|
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)
|
|
279
416
|
except Exception as e:
|
|
280
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
|
+
)
|
|
281
421
|
time.sleep(REQUEST_RETRY_DELAY)
|
|
282
|
-
self.
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
285
426
|
|
|
286
427
|
return None
|
|
287
428
|
|
|
288
429
|
# end method definition
|
|
289
430
|
|
|
290
|
-
def scroll_to_element(self, element:
|
|
431
|
+
def scroll_to_element(self, element: Locator) -> None:
|
|
291
432
|
"""Scroll an element into view to make it clickable.
|
|
292
433
|
|
|
293
434
|
Args:
|
|
294
|
-
element (
|
|
435
|
+
element (Locator):
|
|
295
436
|
Web element that has been identified before.
|
|
296
437
|
|
|
297
438
|
"""
|
|
@@ -307,41 +448,28 @@ class BrowserAutomation:
|
|
|
307
448
|
|
|
308
449
|
# end method definition
|
|
309
450
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
selector: str,
|
|
313
|
-
selector_type: str = "id",
|
|
314
|
-
role_type: str | None = None,
|
|
315
|
-
show_error: bool = True,
|
|
316
|
-
) -> ElementHandle | None:
|
|
317
|
-
"""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.
|
|
318
453
|
|
|
319
454
|
Args:
|
|
320
455
|
selector (str):
|
|
321
|
-
The
|
|
322
|
-
selector_type (str
|
|
456
|
+
The selector to find the element on the page.
|
|
457
|
+
selector_type (str):
|
|
323
458
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
324
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
|
|
325
467
|
role_type (str | None, optional):
|
|
326
468
|
ARIA role when using selector_type="role", e.g., "button", "textbox".
|
|
327
469
|
If irrelevant then None should be passed for role_type.
|
|
328
|
-
show_error (bool, optional):
|
|
329
|
-
Show an error if not found or not visible.
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
ElementHandle:
|
|
333
|
-
The web element or None in case an error occured.
|
|
334
470
|
|
|
335
471
|
"""
|
|
336
472
|
|
|
337
|
-
locator = None
|
|
338
|
-
failure_message = "Cannot find page element with selector -> '{}' ({}){}".format(
|
|
339
|
-
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
340
|
-
)
|
|
341
|
-
success_message = "Found page element with selector -> '{}' ('{}'){}".format(
|
|
342
|
-
selector, selector_type, " and role type -> '{}'".format(role_type) if role_type else ""
|
|
343
|
-
)
|
|
344
|
-
|
|
345
473
|
try:
|
|
346
474
|
match selector_type:
|
|
347
475
|
case "id":
|
|
@@ -355,7 +483,8 @@ class BrowserAutomation:
|
|
|
355
483
|
case "css":
|
|
356
484
|
locator = self.page.locator(selector)
|
|
357
485
|
case "text":
|
|
358
|
-
locator = self.page.get_by_text(selector)
|
|
486
|
+
# locator = self.page.get_by_text(selector)
|
|
487
|
+
locator = self.page.locator("text={}".format(selector))
|
|
359
488
|
case "title":
|
|
360
489
|
locator = self.page.get_by_title(selector)
|
|
361
490
|
case "label":
|
|
@@ -373,23 +502,83 @@ class BrowserAutomation:
|
|
|
373
502
|
self.logger.error("Unsupported selector type -> '%s'", selector_type)
|
|
374
503
|
return None
|
|
375
504
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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)
|
|
382
556
|
else:
|
|
383
|
-
self.logger.
|
|
557
|
+
self.logger.warning(failure_message)
|
|
558
|
+
return None
|
|
384
559
|
|
|
385
|
-
|
|
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:
|
|
386
573
|
if show_error:
|
|
387
|
-
self.logger.error("%s
|
|
574
|
+
self.logger.error("%s (timeout)", failure_message)
|
|
388
575
|
else:
|
|
389
|
-
self.logger.warning("%s
|
|
576
|
+
self.logger.warning("%s (timeout)", failure_message)
|
|
390
577
|
return None
|
|
578
|
+
else:
|
|
579
|
+
self.logger.debug(success_message)
|
|
391
580
|
|
|
392
|
-
return
|
|
581
|
+
return locator
|
|
393
582
|
|
|
394
583
|
# end method definition
|
|
395
584
|
|
|
@@ -401,7 +590,10 @@ class BrowserAutomation:
|
|
|
401
590
|
scroll_to_element: bool = True,
|
|
402
591
|
desired_checkbox_state: bool | None = None,
|
|
403
592
|
is_navigation_trigger: bool = False,
|
|
593
|
+
is_popup_trigger: bool = False,
|
|
594
|
+
is_page_close_trigger: bool = False,
|
|
404
595
|
wait_until: str | None = None,
|
|
596
|
+
wait_time: float = 0.0,
|
|
405
597
|
show_error: bool = True,
|
|
406
598
|
) -> bool:
|
|
407
599
|
"""Find a page element and click it.
|
|
@@ -422,15 +614,22 @@ class BrowserAutomation:
|
|
|
422
614
|
If None then click it in any case.
|
|
423
615
|
is_navigation_trigger (bool, optional):
|
|
424
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?
|
|
425
621
|
wait_until (str | None, optional):
|
|
426
622
|
Wait until a certain condition. Options are:
|
|
427
|
-
* "
|
|
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)
|
|
428
625
|
This is the safest strategy for pages that keep loading content in the background
|
|
429
626
|
like Salesforce.
|
|
430
|
-
* "networkidle" -
|
|
627
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
431
628
|
This seems to be the safest one for OpenText Content Server.
|
|
432
|
-
* "domcontentloaded" -
|
|
629
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
433
630
|
but subresources may still load).
|
|
631
|
+
wait_time (float):
|
|
632
|
+
Time in seconds to wait for elements to appear.
|
|
434
633
|
show_error (bool, optional):
|
|
435
634
|
Show an error if the element is not found or not clickable.
|
|
436
635
|
|
|
@@ -441,11 +640,19 @@ class BrowserAutomation:
|
|
|
441
640
|
|
|
442
641
|
"""
|
|
443
642
|
|
|
643
|
+
success = True # Final return value
|
|
644
|
+
|
|
444
645
|
# If no specific wait until strategy is provided in the
|
|
445
646
|
# parameter, we take the one from the browser automation class:
|
|
446
647
|
if wait_until is None:
|
|
447
648
|
wait_until = self.wait_until
|
|
448
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
|
+
|
|
449
656
|
if not selector:
|
|
450
657
|
failure_message = "Missing element selector! Cannot find page element!"
|
|
451
658
|
if show_error:
|
|
@@ -464,74 +671,74 @@ class BrowserAutomation:
|
|
|
464
671
|
if scroll_to_element:
|
|
465
672
|
self.scroll_to_element(elem)
|
|
466
673
|
|
|
467
|
-
# Handle checkboxes
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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,
|
|
476
689
|
)
|
|
477
|
-
return True # No need to click
|
|
478
|
-
else:
|
|
479
|
-
self.logger.debug("Checkbox -> '%s' has state mismatch. Clicking to change state.", selector)
|
|
480
|
-
|
|
481
|
-
if is_navigation_trigger:
|
|
482
|
-
self.logger.info("Clicking on navigation-triggering element -> '%s'", selector)
|
|
483
|
-
try:
|
|
484
690
|
with self.page.expect_navigation(wait_until=wait_until):
|
|
485
691
|
elem.click()
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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)...",
|
|
489
705
|
selector,
|
|
490
|
-
|
|
706
|
+
"selector type -> '{}'".format(selector_type),
|
|
707
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
491
708
|
)
|
|
492
|
-
return False
|
|
493
|
-
else:
|
|
494
|
-
self.logger.info("Clicking on non-navigating element -> '%s'", selector)
|
|
495
|
-
try:
|
|
496
709
|
elem.click()
|
|
497
710
|
time.sleep(1)
|
|
498
|
-
|
|
499
|
-
self.logger.error("Click failed -> %s", str(e))
|
|
500
|
-
return False
|
|
501
|
-
|
|
502
|
-
if is_checkbox and desired_checkbox_state is not None:
|
|
503
|
-
elem = self.find_elem(selector=selector, selector_type=selector_type, show_error=show_error)
|
|
504
|
-
if elem:
|
|
505
|
-
checkbox_state = elem.is_checked()
|
|
506
|
-
|
|
507
|
-
if checkbox_state is not None:
|
|
508
|
-
if checkbox_state == desired_checkbox_state:
|
|
711
|
+
if success:
|
|
509
712
|
self.logger.debug(
|
|
510
|
-
"Successfully clicked
|
|
713
|
+
"Successfully clicked element -> '%s' (%s%s)",
|
|
511
714
|
selector,
|
|
512
|
-
|
|
715
|
+
"selector type -> '{}'".format(selector_type),
|
|
716
|
+
", role type -> '{}'".format(role_type) if role_type else "",
|
|
513
717
|
)
|
|
514
|
-
else:
|
|
515
|
-
self.logger.error(
|
|
516
|
-
"Failed to flip checkbox element -> '%s' to desired state. It's state is still -> %s and not -> %s",
|
|
517
|
-
selector,
|
|
518
|
-
checkbox_state,
|
|
519
|
-
desired_checkbox_state,
|
|
520
|
-
)
|
|
521
|
-
else:
|
|
522
|
-
self.logger.debug("Successfully clicked element -> '%s'", selector)
|
|
523
|
-
|
|
524
|
-
if self.take_screenshots:
|
|
525
|
-
self.take_screenshot()
|
|
526
718
|
|
|
527
719
|
except PlaywrightError as e:
|
|
528
720
|
if show_error:
|
|
529
|
-
self.logger.error(
|
|
721
|
+
self.logger.error(
|
|
722
|
+
"Cannot click page element -> '%s' (%s); error -> %s", selector, selector_type, str(e)
|
|
723
|
+
)
|
|
530
724
|
else:
|
|
531
|
-
self.logger.warning(
|
|
532
|
-
|
|
725
|
+
self.logger.warning(
|
|
726
|
+
"Cannot click page element -> '%s' (%s); warning -> %s", selector, selector_type, str(e)
|
|
727
|
+
)
|
|
728
|
+
success = not show_error
|
|
533
729
|
|
|
534
|
-
|
|
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
|
|
535
742
|
|
|
536
743
|
# end method definition
|
|
537
744
|
|
|
@@ -568,6 +775,8 @@ class BrowserAutomation:
|
|
|
568
775
|
|
|
569
776
|
"""
|
|
570
777
|
|
|
778
|
+
success = False # Final return value
|
|
779
|
+
|
|
571
780
|
elem = self.find_elem(selector=selector, selector_type=selector_type, role_type=role_type, show_error=True)
|
|
572
781
|
if not elem:
|
|
573
782
|
return False
|
|
@@ -582,12 +791,14 @@ class BrowserAutomation:
|
|
|
582
791
|
else:
|
|
583
792
|
self.logger.warning(message)
|
|
584
793
|
|
|
794
|
+
if self.take_screenshots:
|
|
795
|
+
self.take_screenshot()
|
|
796
|
+
|
|
585
797
|
return False
|
|
586
798
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
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
|
+
)
|
|
591
802
|
|
|
592
803
|
try:
|
|
593
804
|
# HTML '<select>' can only be identified based on its tag name:
|
|
@@ -596,27 +807,38 @@ class BrowserAutomation:
|
|
|
596
807
|
input_type = elem.get_attribute("type")
|
|
597
808
|
|
|
598
809
|
if tag_name == "select":
|
|
599
|
-
options = elem.
|
|
600
|
-
|
|
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
|
+
|
|
601
814
|
if value not in option_values:
|
|
602
815
|
self.logger.warning(
|
|
603
|
-
"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!",
|
|
604
817
|
value,
|
|
605
818
|
option_values,
|
|
606
819
|
)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
820
|
+
else:
|
|
821
|
+
# We set the value over the (visible) label:
|
|
822
|
+
elem.select_option(label=value)
|
|
823
|
+
success = True
|
|
610
824
|
elif tag_name == "input" and input_type == "checkbox":
|
|
611
825
|
# Handle checkbox
|
|
612
826
|
if not isinstance(value, bool):
|
|
613
827
|
self.logger.error("Checkbox value must be a boolean!")
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
618
839
|
else:
|
|
619
840
|
elem.fill(value)
|
|
841
|
+
success = True
|
|
620
842
|
except PlaywrightError as e:
|
|
621
843
|
message = "Cannot set page element selected by -> '{}' ({}) to value -> '{}'; error -> {}".format(
|
|
622
844
|
selector, selector_type, value, str(e)
|
|
@@ -625,12 +847,12 @@ class BrowserAutomation:
|
|
|
625
847
|
self.logger.error(message)
|
|
626
848
|
else:
|
|
627
849
|
self.logger.warning(message)
|
|
628
|
-
|
|
850
|
+
success = not show_error
|
|
629
851
|
|
|
630
852
|
if self.take_screenshots:
|
|
631
853
|
self.take_screenshot()
|
|
632
854
|
|
|
633
|
-
return
|
|
855
|
+
return success
|
|
634
856
|
|
|
635
857
|
# end method definition
|
|
636
858
|
|
|
@@ -692,13 +914,14 @@ class BrowserAutomation:
|
|
|
692
914
|
substring: bool = True,
|
|
693
915
|
min_count: int = 1,
|
|
694
916
|
wait_time: float = 0.0,
|
|
917
|
+
wait_state: str = "visible",
|
|
695
918
|
show_error: bool = True,
|
|
696
919
|
) -> tuple[bool | None, int]:
|
|
697
920
|
"""Check if (multiple) elements with defined attributes exist on page and return the number.
|
|
698
921
|
|
|
699
922
|
Args:
|
|
700
923
|
selector (str):
|
|
701
|
-
|
|
924
|
+
The selector to find the element on the page.
|
|
702
925
|
selector_type (str):
|
|
703
926
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
704
927
|
"label", "placeholder", "alt".
|
|
@@ -722,6 +945,9 @@ class BrowserAutomation:
|
|
|
722
945
|
Minimum number of required matches (# elements on page).
|
|
723
946
|
wait_time (float):
|
|
724
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).
|
|
725
951
|
show_error (bool):
|
|
726
952
|
Whether to log warnings/errors.
|
|
727
953
|
|
|
@@ -734,86 +960,126 @@ class BrowserAutomation:
|
|
|
734
960
|
|
|
735
961
|
"""
|
|
736
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
|
+
|
|
737
1018
|
# Some operations that are done server-side and dynamically update
|
|
738
|
-
# the page may require a waiting time:
|
|
1019
|
+
# the page with additional matching elements that may require a waiting time:
|
|
739
1020
|
if wait_time > 0.0:
|
|
740
|
-
self.logger.info("Wait
|
|
1021
|
+
self.logger.info("Wait additional %d milliseconds before checking...", wait_time * 1000)
|
|
741
1022
|
self.page.wait_for_timeout(wait_time * 1000)
|
|
742
1023
|
|
|
743
|
-
|
|
744
|
-
match selector_type:
|
|
745
|
-
case "id":
|
|
746
|
-
locator = self.page.locator("#{}".format(selector))
|
|
747
|
-
case "name":
|
|
748
|
-
locator = self.page.locator("[name='{}']".format(selector))
|
|
749
|
-
case "class_name":
|
|
750
|
-
locator = self.page.locator(".{}".format(selector))
|
|
751
|
-
case "xpath":
|
|
752
|
-
locator = self.page.locator("xpath={}".format(selector))
|
|
753
|
-
case "css":
|
|
754
|
-
locator = self.page.locator(selector)
|
|
755
|
-
case "text":
|
|
756
|
-
locator = self.page.get_by_text(selector)
|
|
757
|
-
case "title":
|
|
758
|
-
locator = self.page.get_by_title(selector)
|
|
759
|
-
case "label":
|
|
760
|
-
locator = self.page.get_by_label(selector)
|
|
761
|
-
case "placeholder":
|
|
762
|
-
locator = self.page.get_by_placeholder(selector)
|
|
763
|
-
case "alt":
|
|
764
|
-
locator = self.page.get_by_alt_text(selector)
|
|
765
|
-
case "role":
|
|
766
|
-
if not role_type:
|
|
767
|
-
self.logger.error("Role type must be specified when using find method 'role'!")
|
|
768
|
-
return (None, 0)
|
|
769
|
-
locator = self.page.get_by_role(role=role_type, name=selector)
|
|
770
|
-
case _:
|
|
771
|
-
self.logger.error("Unsupported selector type -> '%s'", selector_type)
|
|
772
|
-
return (None, 0)
|
|
1024
|
+
matching_elems = []
|
|
773
1025
|
|
|
774
|
-
|
|
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)
|
|
775
1031
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
)
|
|
781
1039
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if
|
|
785
|
-
|
|
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
|
+
)
|
|
786
1047
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1048
|
+
for i in range(count):
|
|
1049
|
+
elem = locator.nth(i)
|
|
1050
|
+
if not elem:
|
|
1051
|
+
continue
|
|
791
1052
|
|
|
792
|
-
|
|
793
|
-
|
|
1053
|
+
if value is None:
|
|
1054
|
+
# No filtering, accept all elements
|
|
1055
|
+
matching_elems.append(elem)
|
|
1056
|
+
continue
|
|
794
1057
|
|
|
795
|
-
|
|
796
|
-
|
|
1058
|
+
# Get attribute or text content
|
|
1059
|
+
attr_value = elem.get_attribute(attribute) if attribute else elem.text_content()
|
|
797
1060
|
|
|
798
|
-
|
|
799
|
-
|
|
1061
|
+
if not attr_value:
|
|
1062
|
+
continue
|
|
800
1063
|
|
|
801
|
-
|
|
1064
|
+
if (substring and value in attr_value) or (not substring and value == attr_value):
|
|
1065
|
+
matching_elems.append(elem)
|
|
802
1066
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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",
|
|
806
1076
|
"Only {}".format(matching_elements_count) if matching_elems else "No",
|
|
1077
|
+
"s" if matching_elements_count > 1 else "",
|
|
807
1078
|
min_count,
|
|
808
1079
|
)
|
|
809
|
-
|
|
1080
|
+
self.logger.info("Found %d matching elements.", matching_elements_count)
|
|
810
1081
|
|
|
811
|
-
|
|
812
|
-
if show_error:
|
|
813
|
-
self.logger.error("Failed to check if elements -> '%s' exist; errors -> %s", selector, str(e))
|
|
814
|
-
return (None, 0)
|
|
815
|
-
|
|
816
|
-
return (True, matching_elements_count)
|
|
1082
|
+
return (success, matching_elements_count)
|
|
817
1083
|
|
|
818
1084
|
# end method definition
|
|
819
1085
|
|
|
@@ -839,12 +1105,13 @@ class BrowserAutomation:
|
|
|
839
1105
|
The URL to the login page. Defaults to "".
|
|
840
1106
|
wait_until (str | None, optional):
|
|
841
1107
|
Wait until a certain condition. Options are:
|
|
842
|
-
* "
|
|
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)
|
|
843
1110
|
This is the safest strategy for pages that keep loading content in the background
|
|
844
1111
|
like Salesforce.
|
|
845
|
-
* "networkidle" -
|
|
1112
|
+
* "networkidle" - waits until there are no network connections for at least 500 ms.
|
|
846
1113
|
This seems to be the safest one for OpenText Content Server.
|
|
847
|
-
* "domcontentloaded" -
|
|
1114
|
+
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
848
1115
|
but subresources may still load).
|
|
849
1116
|
selector_type (str, optional):
|
|
850
1117
|
One of "id", "name", "class_name", "xpath", "css", "role", "text", "title",
|
|
@@ -880,12 +1147,14 @@ class BrowserAutomation:
|
|
|
880
1147
|
)
|
|
881
1148
|
return False
|
|
882
1149
|
|
|
1150
|
+
self.logger.info("Wait for -> '%s' to assure login is completed and target page is loaded.", wait_until)
|
|
883
1151
|
self.page.wait_for_load_state(wait_until)
|
|
884
1152
|
|
|
885
1153
|
title = self.get_title()
|
|
886
1154
|
if not title:
|
|
887
1155
|
self.logger.error(
|
|
888
|
-
"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,
|
|
889
1158
|
)
|
|
890
1159
|
return False
|
|
891
1160
|
|
|
@@ -896,6 +1165,7 @@ class BrowserAutomation:
|
|
|
896
1165
|
self.logger.error("Authentication failed. You may have given the wrong password!")
|
|
897
1166
|
return False
|
|
898
1167
|
|
|
1168
|
+
self.logger.info("Login completed successfully! Page title -> '%s'", title)
|
|
899
1169
|
self.logged_in = True
|
|
900
1170
|
|
|
901
1171
|
return True
|
|
@@ -924,10 +1194,47 @@ class BrowserAutomation:
|
|
|
924
1194
|
def end_session(self) -> None:
|
|
925
1195
|
"""End the browser session and close the browser."""
|
|
926
1196
|
|
|
927
|
-
self.logger.
|
|
1197
|
+
self.logger.info("Close Browser Page...")
|
|
1198
|
+
self.page.close()
|
|
1199
|
+
self.logger.info("Close Browser Context...")
|
|
928
1200
|
self.context.close()
|
|
1201
|
+
self.logger.info("Close Browser...")
|
|
929
1202
|
self.browser.close()
|
|
930
1203
|
self.logged_in = False
|
|
1204
|
+
self.logger.info("Stop Playwright instance...")
|
|
931
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()
|
|
932
1239
|
|
|
933
1240
|
# end method definition
|