pydoll-python 2.17.0__tar.gz → 2.18.0__tar.gz
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.
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/PKG-INFO +1 -28
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/README.md +0 -27
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/mixins/find_elements_mixin.py +101 -52
- pydoll_python-2.18.0/pydoll/elements/utils/__init__.py +3 -0
- pydoll_python-2.18.0/pydoll/elements/utils/selector_parser.py +448 -0
- pydoll_python-2.18.0/pydoll/utils/__init__.py +25 -0
- pydoll_python-2.18.0/pydoll/utils/socks5_proxy_forwarder.py +592 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pyproject.toml +1 -1
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/LICENSE +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/base.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/chrome.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/edge.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/interfaces.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/proxy_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/options.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/request.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/response.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/tab.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/browser_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/dom_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/fetch_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/input_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/network_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/page_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/runtime_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/storage_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/target_commands.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/connection_handler.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/commands_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/events_manager.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/constants.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/decorators.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/mixins/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/shadow_root.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/web_element.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/exceptions.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/iframe.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/keyboard.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/scroll.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/base.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/debugger/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/emulation/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/io/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/security/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/__init__.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/events.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/methods.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/types.py +0 -0
- {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/py.typed +0 -0
- /pydoll_python-2.17.0/pydoll/utils.py → /pydoll_python-2.18.0/pydoll/utils/general.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoll-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.18.0
|
|
4
4
|
Summary: Pydoll is a library for automating chromium-based browsers without a WebDriver, offering realistic interactions.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Thalison Fernandes
|
|
@@ -124,33 +124,6 @@ for sr in shadow_roots:
|
|
|
124
124
|
[**📖 Shadow DOM Docs**](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/)
|
|
125
125
|
</details>
|
|
126
126
|
|
|
127
|
-
<details>
|
|
128
|
-
<summary><b>Cloudflare Turnstile Bypass: Faster, More Robust, Fully Automatic</b></summary>
|
|
129
|
-
<br>
|
|
130
|
-
|
|
131
|
-
The Cloudflare Turnstile bypass has been **completely rewritten** using shadow root traversal, making it significantly more reliable than the old selector-based approach.
|
|
132
|
-
|
|
133
|
-
**What changed:**
|
|
134
|
-
|
|
135
|
-
- **Automatic detection** — Pydoll now polls the page's shadow DOM for the Cloudflare challenge domain, no selectors needed
|
|
136
|
-
- **Full shadow root traversal** — navigates outer shadow root → cross-origin iframe → inner shadow root → actual checkbox element
|
|
137
|
-
- **No more artificial delays** — the old `time_before_click` sleep is gone; the checkbox is clicked as soon as it's found
|
|
138
|
-
- **Zero configuration** — just call the method, Pydoll handles the rest
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
async with Chrome() as browser:
|
|
142
|
-
tab = await browser.start()
|
|
143
|
-
|
|
144
|
-
# That's it — fully automatic
|
|
145
|
-
async with tab.expect_and_bypass_cloudflare_captcha():
|
|
146
|
-
await tab.go_to('https://site-with-turnstile.com')
|
|
147
|
-
|
|
148
|
-
print("Captcha handled!")
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
[**📖 Cloudflare Turnstile Docs**](https://pydoll.tech/docs/features/advanced/behavioral-captcha-bypass/)
|
|
152
|
-
</details>
|
|
153
|
-
|
|
154
127
|
<details>
|
|
155
128
|
<summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
|
|
156
129
|
<br>
|
|
@@ -104,33 +104,6 @@ for sr in shadow_roots:
|
|
|
104
104
|
[**📖 Shadow DOM Docs**](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/)
|
|
105
105
|
</details>
|
|
106
106
|
|
|
107
|
-
<details>
|
|
108
|
-
<summary><b>Cloudflare Turnstile Bypass: Faster, More Robust, Fully Automatic</b></summary>
|
|
109
|
-
<br>
|
|
110
|
-
|
|
111
|
-
The Cloudflare Turnstile bypass has been **completely rewritten** using shadow root traversal, making it significantly more reliable than the old selector-based approach.
|
|
112
|
-
|
|
113
|
-
**What changed:**
|
|
114
|
-
|
|
115
|
-
- **Automatic detection** — Pydoll now polls the page's shadow DOM for the Cloudflare challenge domain, no selectors needed
|
|
116
|
-
- **Full shadow root traversal** — navigates outer shadow root → cross-origin iframe → inner shadow root → actual checkbox element
|
|
117
|
-
- **No more artificial delays** — the old `time_before_click` sleep is gone; the checkbox is clicked as soon as it's found
|
|
118
|
-
- **Zero configuration** — just call the method, Pydoll handles the rest
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
async with Chrome() as browser:
|
|
122
|
-
tab = await browser.start()
|
|
123
|
-
|
|
124
|
-
# That's it — fully automatic
|
|
125
|
-
async with tab.expect_and_bypass_cloudflare_captcha():
|
|
126
|
-
await tab.go_to('https://site-with-turnstile.com')
|
|
127
|
-
|
|
128
|
-
print("Captcha handled!")
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
[**📖 Cloudflare Turnstile Docs**](https://pydoll.tech/docs/features/advanced/behavioral-captcha-bypass/)
|
|
132
|
-
</details>
|
|
133
|
-
|
|
134
107
|
<details>
|
|
135
108
|
<summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
|
|
136
109
|
<br>
|
|
@@ -10,8 +10,8 @@ from pydoll.commands import (
|
|
|
10
10
|
)
|
|
11
11
|
from pydoll.connection.connection_handler import ConnectionHandler
|
|
12
12
|
from pydoll.constants import By, Scripts
|
|
13
|
+
from pydoll.elements.utils import SelectorParser
|
|
13
14
|
from pydoll.exceptions import ElementNotFound, WaitElementTimeout
|
|
14
|
-
from pydoll.utils import normalize_synthetic_xpath
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from typing import Literal, Optional, Union
|
|
@@ -65,29 +65,7 @@ class FindElementsMixin:
|
|
|
65
65
|
"""
|
|
66
66
|
Build JS expression using Scripts to extract textContent based on selector type.
|
|
67
67
|
"""
|
|
68
|
-
|
|
69
|
-
method_lc = (method or '').lower()
|
|
70
|
-
|
|
71
|
-
if 'xpath' in method_lc:
|
|
72
|
-
normalized_xpath = normalize_synthetic_xpath(raw)
|
|
73
|
-
escaped_xpath = normalized_xpath.replace('"', '\\"')
|
|
74
|
-
return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', escaped_xpath)
|
|
75
|
-
|
|
76
|
-
if method_lc == 'name':
|
|
77
|
-
escaped_name = raw.replace('"', '\\"')
|
|
78
|
-
xpath = f'//*[@name="{escaped_name}"]'
|
|
79
|
-
return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', xpath)
|
|
80
|
-
|
|
81
|
-
escaped = raw.replace('\\', '\\\\').replace('"', '\\"')
|
|
82
|
-
if method_lc == 'id':
|
|
83
|
-
css = f'#{escaped}'
|
|
84
|
-
elif method_lc == 'class_name':
|
|
85
|
-
css = f'.{escaped}'
|
|
86
|
-
elif method_lc == 'tag_name':
|
|
87
|
-
css = escaped
|
|
88
|
-
else:
|
|
89
|
-
css = escaped
|
|
90
|
-
return Scripts.GET_TEXT_BY_CSS.replace('{selector}', css)
|
|
68
|
+
return SelectorParser.build_text_expression(selector, method)
|
|
91
69
|
|
|
92
70
|
@overload
|
|
93
71
|
async def find(
|
|
@@ -344,6 +322,17 @@ class FindElementsMixin:
|
|
|
344
322
|
f'find_or_wait_element(): by={by}, value={value}, timeout={timeout}, '
|
|
345
323
|
f'find_all={find_all}, raise_exc={raise_exc}'
|
|
346
324
|
)
|
|
325
|
+
|
|
326
|
+
if by == By.XPATH:
|
|
327
|
+
segments = SelectorParser.parse_iframe_segments_xpath(value)
|
|
328
|
+
elif by == By.CSS_SELECTOR:
|
|
329
|
+
segments = SelectorParser.parse_iframe_segments_css(value)
|
|
330
|
+
else:
|
|
331
|
+
segments = [(by, value)]
|
|
332
|
+
|
|
333
|
+
if len(segments) > 1:
|
|
334
|
+
return await self._find_across_iframes(segments, timeout, find_all, raise_exc)
|
|
335
|
+
|
|
347
336
|
find_method = self._find_element if not find_all else self._find_elements
|
|
348
337
|
start_time = asyncio.get_event_loop().time()
|
|
349
338
|
|
|
@@ -371,6 +360,90 @@ class FindElementsMixin:
|
|
|
371
360
|
|
|
372
361
|
await asyncio.sleep(0.5)
|
|
373
362
|
|
|
363
|
+
async def _find_across_iframes(
|
|
364
|
+
self,
|
|
365
|
+
segments: list[tuple[By, str]],
|
|
366
|
+
timeout: int,
|
|
367
|
+
find_all: bool,
|
|
368
|
+
raise_exc: bool,
|
|
369
|
+
) -> Union[WebElement, list[WebElement], None]:
|
|
370
|
+
"""
|
|
371
|
+
Retry loop for iframe-crossing element searches.
|
|
372
|
+
|
|
373
|
+
Repeatedly calls :meth:`_attempt_find_across_iframes` until the target
|
|
374
|
+
element is found or the *timeout* expires.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
segments: Ordered ``(By, selector)`` pairs — one per iframe boundary
|
|
378
|
+
plus a final selector for the target element(s).
|
|
379
|
+
timeout: Maximum seconds to wait (0 = single attempt).
|
|
380
|
+
find_all: If ``True``, the last segment uses ``_find_elements``.
|
|
381
|
+
raise_exc: Whether to raise on failure.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The found element(s), or ``None`` / ``[]`` on failure.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ElementNotFound: If ``timeout=0``, nothing found, and ``raise_exc=True``.
|
|
388
|
+
WaitElementTimeout: If timeout expires and ``raise_exc=True``.
|
|
389
|
+
"""
|
|
390
|
+
start_time = asyncio.get_event_loop().time()
|
|
391
|
+
selector_repr = ' -> '.join(seg for _, seg in segments)
|
|
392
|
+
|
|
393
|
+
while True:
|
|
394
|
+
result = await self._attempt_find_across_iframes(segments, find_all)
|
|
395
|
+
if result is not None and result != []:
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
if not timeout:
|
|
399
|
+
if raise_exc:
|
|
400
|
+
raise ElementNotFound(f'Element not found across iframes: {selector_repr}')
|
|
401
|
+
return [] if find_all else None
|
|
402
|
+
|
|
403
|
+
if asyncio.get_event_loop().time() - start_time > timeout:
|
|
404
|
+
if raise_exc:
|
|
405
|
+
raise WaitElementTimeout(
|
|
406
|
+
f'Timed out after {timeout}s waiting for element '
|
|
407
|
+
f'across iframes: {selector_repr}'
|
|
408
|
+
)
|
|
409
|
+
return [] if find_all else None
|
|
410
|
+
|
|
411
|
+
await asyncio.sleep(0.5)
|
|
412
|
+
|
|
413
|
+
async def _attempt_find_across_iframes(
|
|
414
|
+
self,
|
|
415
|
+
segments: list[tuple[By, str]],
|
|
416
|
+
find_all: bool,
|
|
417
|
+
) -> Union[WebElement, list[WebElement], None]:
|
|
418
|
+
"""
|
|
419
|
+
Single attempt to walk iframe segments and find the target element.
|
|
420
|
+
|
|
421
|
+
For each intermediate segment, finds a single iframe element and uses it
|
|
422
|
+
as the search context for the next segment. The last segment respects
|
|
423
|
+
*find_all*.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
segments: Ordered ``(By, selector)`` pairs.
|
|
427
|
+
find_all: Whether the final segment should return all matches.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Found element(s) or ``None`` / ``[]`` if any intermediate step fails.
|
|
431
|
+
"""
|
|
432
|
+
current_context: FindElementsMixin = self
|
|
433
|
+
for i, (by, selector) in enumerate(segments):
|
|
434
|
+
is_last = i == len(segments) - 1
|
|
435
|
+
if is_last:
|
|
436
|
+
if find_all:
|
|
437
|
+
result = await current_context._find_elements(by, selector, raise_exc=False)
|
|
438
|
+
return result if result else []
|
|
439
|
+
return await current_context._find_element(by, selector, raise_exc=False)
|
|
440
|
+
|
|
441
|
+
element = await current_context._find_element(by, selector, raise_exc=False)
|
|
442
|
+
if not element or not getattr(element, 'is_iframe', False):
|
|
443
|
+
return None
|
|
444
|
+
current_context = element
|
|
445
|
+
return None
|
|
446
|
+
|
|
374
447
|
async def _find_element(
|
|
375
448
|
self, by: By, value: str, raise_exc: bool = True
|
|
376
449
|
) -> Optional[WebElement]:
|
|
@@ -568,7 +641,7 @@ class FindElementsMixin:
|
|
|
568
641
|
name: Optional[str] = None,
|
|
569
642
|
tag_name: Optional[str] = None,
|
|
570
643
|
text: Optional[str] = None,
|
|
571
|
-
**attributes,
|
|
644
|
+
**attributes: str,
|
|
572
645
|
) -> str:
|
|
573
646
|
"""
|
|
574
647
|
Build XPath expression from multiple attribute criteria.
|
|
@@ -581,28 +654,7 @@ class FindElementsMixin:
|
|
|
581
654
|
Attribute names with underscores are automatically converted to hyphens
|
|
582
655
|
to match HTML attribute naming conventions (e.g., data_test -> data-test).
|
|
583
656
|
"""
|
|
584
|
-
|
|
585
|
-
base_xpath = f'//{tag_name}' if tag_name else '//*'
|
|
586
|
-
if id:
|
|
587
|
-
xpath_conditions.append(f'@id="{id}"')
|
|
588
|
-
if class_name:
|
|
589
|
-
xpath_conditions.append(
|
|
590
|
-
f'contains(concat(" ", normalize-space(@class), " "), " {class_name} ")'
|
|
591
|
-
)
|
|
592
|
-
if name:
|
|
593
|
-
xpath_conditions.append(f'@name="{name}"')
|
|
594
|
-
if text:
|
|
595
|
-
xpath_conditions.append(f'contains(text(), "{text}")')
|
|
596
|
-
for attribute, value in attributes.items():
|
|
597
|
-
# Convert underscores to hyphens for HTML attribute names
|
|
598
|
-
html_attribute = attribute.replace('_', '-')
|
|
599
|
-
xpath_conditions.append(f'@{html_attribute}="{value}"')
|
|
600
|
-
|
|
601
|
-
xpath = (
|
|
602
|
-
f'{base_xpath}[{" and ".join(xpath_conditions)}]' if xpath_conditions else base_xpath
|
|
603
|
-
)
|
|
604
|
-
logger.debug(f'_build_xpath() -> {xpath}')
|
|
605
|
-
return xpath
|
|
657
|
+
return SelectorParser.build_xpath(id, class_name, name, tag_name, text, **attributes)
|
|
606
658
|
|
|
607
659
|
@staticmethod
|
|
608
660
|
def _get_expression_type(expression: str) -> By:
|
|
@@ -613,10 +665,7 @@ class FindElementsMixin:
|
|
|
613
665
|
- XPath: starts with ./, or /
|
|
614
666
|
- Default: CSS_SELECTOR
|
|
615
667
|
"""
|
|
616
|
-
|
|
617
|
-
return By.XPATH
|
|
618
|
-
|
|
619
|
-
return By.CSS_SELECTOR
|
|
668
|
+
return SelectorParser.get_expression_type(expression)
|
|
620
669
|
|
|
621
670
|
async def _describe_node(self, object_id: str = '') -> Node:
|
|
622
671
|
"""
|
|
@@ -834,7 +883,7 @@ class FindElementsMixin:
|
|
|
834
883
|
|
|
835
884
|
Converts absolute XPath to relative for context-based searches.
|
|
836
885
|
"""
|
|
837
|
-
return
|
|
886
|
+
return SelectorParser.ensure_relative_xpath(xpath)
|
|
838
887
|
|
|
839
888
|
@staticmethod
|
|
840
889
|
def _has_object_id_key(response: Union[EvaluateResponse, CallFunctionOnResponse]) -> bool:
|