Skip to content

Commit 085995c

Browse files
authored
Merge pull request #249 from autoscrape-labs/feat/new-public-api
New public methods and some fixes
2 parents 4b55a9e + 7a06064 commit 085995c

File tree

17 files changed

+355
-229
lines changed

17 files changed

+355
-229
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,40 @@ We believe that powerful automation shouldn't require you to become an expert in
4747

4848
## What's New
4949

50+
### WebElement: state waiting and new public APIs
51+
52+
- New `wait_until(...)` on `WebElement` to await element states with minimal code:
53+
54+
```python
55+
# Wait until it becomes visible OR the timeout expires
56+
await element.wait_until(is_visible=True, timeout=5)
57+
58+
# Wait until it becomes interactable (visible, on top, receiving pointer events)
59+
await element.wait_until(is_interactable=True, timeout=10)
60+
```
61+
62+
- Methods now public on `WebElement`:
63+
- `is_visible()`
64+
- Checks that the element has a visible area (> 0), isn’t hidden by CSS and is in the viewport (after `scroll_into_view()` when needed). Useful pre-check before interactions.
65+
- `is_interactable()`
66+
- “Click-ready” state: combines visibility, enabledness and pointer-event hit testing. Ideal for robust flows that avoid lost clicks.
67+
- `is_on_top()`
68+
- Verifies the element is the top hit-test target at the intended click point, avoiding overlays.
69+
- `execute_script(script: str, return_by_value: bool = False)`
70+
- Executes JavaScript in the element’s own context (where `this` is the element). Great for fine-tuning and quick inspections.
71+
72+
```python
73+
# Visually outline the element via JS
74+
await element.execute_script("this.style.outline='2px solid #22d3ee'")
75+
76+
# Confirm states
77+
visible = await element.is_visible()
78+
interactable = await element.is_interactable()
79+
on_top = await element.is_on_top()
80+
```
81+
82+
These additions simplify waiting and state validation before clicking/typing, reducing flakiness and making automations more predictable.
83+
5084
### Browser-context HTTP requests - game changer for hybrid automation!
5185
Ever wished you could make HTTP requests that automatically inherit all your browser's session state? **Now you can!**<br>
5286
The `tab.request` property gives you a beautiful `requests`-like interface that executes HTTP calls directly in the browser's JavaScript context. This means every request automatically gets cookies, authentication headers, CORS policies, and session state, just as if the browser made the request itself.

README_zh.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,40 @@ Pydoll 采用全新设计理念,从零构建,直接对接 Chrome DevTools Pr
4949

5050
## 最新功能
5151

52+
### WebElement:状态等待与新的公共 API
53+
54+
- 新增 `wait_until(...)` 用于等待元素状态,使用更简单:
55+
56+
```python
57+
# 等待元素变为可见,直到超时
58+
await element.wait_until(is_visible=True, timeout=5)
59+
60+
# 等待元素变为可交互(可见、位于顶层并可接收事件)
61+
await element.wait_until(is_interactable=True, timeout=10)
62+
```
63+
64+
- 以下 `WebElement` 方法现已公开:
65+
- `is_visible()`
66+
- 判断元素是否具有可见区域、未被 CSS 隐藏,并在需要时滚动进入视口。适用于交互前的快速校验。
67+
- `is_interactable()`
68+
- “可点击”状态:综合可见性、启用状态与指针事件命中等条件,适合构建更稳健的交互流程。
69+
- `is_on_top()`
70+
- 检查元素在点击位置是否为顶部命中目标,避免被覆盖导致点击失效。
71+
- `execute_script(script: str, return_by_value: bool = False)`
72+
- 在元素上下文中执行 JavaScript(this 指向该元素),便于细粒度调整与快速检查。
73+
74+
```python
75+
# 使用 JS 高亮元素
76+
await element.execute_script("this.style.outline='2px solid #22d3ee'")
77+
78+
# 校验状态
79+
visible = await element.is_visible()
80+
interactable = await element.is_interactable()
81+
on_top = await element.is_on_top()
82+
```
83+
84+
以上新增能力能显著简化“等待+验证”场景,降低自动化过程中的不稳定性,使用例更可预测。
85+
5286
### 浏览器上下文 HTTP 请求 - 混合自动化的游戏规则改变者!
5387
你是否曾经希望能够发出自动继承浏览器所有会话状态的 HTTP 请求?**现在你可以了!**<br>
5488
`tab.request` 属性为你提供了一个美观的 `requests` 风格接口,可在浏览器的 JavaScript 上下文中直接执行 HTTP 调用。这意味着每个请求都会自动获得 cookies、身份验证标头、CORS 策略和会话状态,就像浏览器本身发出请求一样。

public/docs/deep-dive/webelement-domain.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ async def click(
217217
if self._is_option_tag():
218218
return await self.click_option_tag()
219219

220-
if not await self._is_element_visible():
220+
if not await self.is_visible():
221221
raise exceptions.ElementNotVisible(
222222
'Element is not visible on the page.'
223223
)
@@ -341,16 +341,16 @@ WebElement provides seamless integration with JavaScript for operations that req
341341

342342
```python
343343
# Execute JavaScript in the context of this element
344-
await element._execute_script("this.style.border = '2px solid red';")
344+
await element.execute_script("this.style.border = '2px solid red';")
345345

346346
# Get result from JavaScript execution
347-
visibility = await element._is_element_visible()
347+
visibility = await element.is_visible()
348348
```
349349

350350
The implementation uses the CDP Runtime domain to execute JavaScript with the element as the context:
351351

352352
```python
353-
async def _execute_script(
353+
async def execute_script(
354354
self, script: str, return_by_value: bool = False
355355
):
356356
"""
@@ -369,10 +369,13 @@ WebElement provides methods to check the element's visibility and interactabilit
369369

370370
```python
371371
# Check if element is visible
372-
is_visible = await element._is_element_visible()
372+
is_visible = await element.is_visible()
373373

374374
# Check if element is the topmost at its position
375-
is_on_top = await element._is_element_on_top()
375+
is_on_top = await element.is_on_top()
376+
377+
# Check if element is interactable
378+
is_interactable = await element.is_interactable()
376379
```
377380

378381
These verifications are crucial for reliable automation, ensuring that elements can be interacted with before attempting operations.

public/docs/zh/deep-dive/webelement-domain.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ async def click(
219219
if self._is_option_tag():
220220
return await self.click_option_tag()
221221

222-
if not await self._is_element_visible():
222+
if not await self.is_visible():
223223
raise exceptions.ElementNotVisible(
224224
'Element is not visible on the page.'
225225
)
@@ -342,16 +342,16 @@ WebElement 为需要直接操作DOM的操作提供了与JavaScript的无缝集
342342

343343
```python
344344
# Execute JavaScript in the context of this element
345-
await element._execute_script("this.style.border = '2px solid red';")
345+
await element.execute_script("this.style.border = '2px solid red';")
346346

347347
# Get result from JavaScript execution
348-
visibility = await element._is_element_visible()
348+
visibility = await element.is_visible()
349349
```
350350

351351
该实现通过CDP Runtime域执行JavaScript,并将元素作为上下文:
352352

353353
```python
354-
async def _execute_script(
354+
async def execute_script(
355355
self, script: str, return_by_value: bool = False
356356
):
357357
"""
@@ -371,10 +371,13 @@ WebElement 提供了检查元素可见性和可交互性的方法:
371371

372372
```python
373373
# Check if element is visible
374-
is_visible = await element._is_element_visible()
374+
is_visible = await element.is_visible()
375375

376376
# Check if element is the topmost at its position
377-
is_on_top = await element._is_element_on_top()
377+
is_on_top = await element.is_on_top()
378+
379+
# Check if element is interactable
380+
is_interactable = await element.is_interactable()
378381
```
379382

380383
这些验证对于实现可靠的自动化至关重要,可在尝试操作前确保元素可被交互。

pydoll/browser/tab.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
PageLoadTimeout,
4747
WaitElementTimeout,
4848
)
49-
from pydoll.protocol.base import EmptyResponse
49+
from pydoll.protocol.base import EmptyResponse, Response
5050
from pydoll.protocol.browser.events import (
5151
BrowserEvent,
5252
DownloadProgressEvent,
@@ -282,7 +282,7 @@ async def enable_fetch_events(
282282
Note:
283283
Intercepted requests must be explicitly continued or timeout.
284284
"""
285-
response: EmptyResponse = await self._execute_command(
285+
response: Response[EmptyResponse] = await self._execute_command(
286286
FetchCommands.enable(
287287
handle_auth_requests=handle_auth,
288288
resource_type=resource_type,
@@ -424,6 +424,10 @@ async def get_frame(self, frame: WebElement) -> IFrame:
424424

425425
return Tab(self._browser, self._connection_port, iframe_target['targetId'])
426426

427+
async def bring_to_front(self):
428+
"""Brings the page to front."""
429+
return await self._execute_command(PageCommands.bring_to_front())
430+
427431
async def get_cookies(self) -> list[Cookie]:
428432
"""Get all cookies accessible from current page."""
429433
response: GetCookiesResponse = await self._execute_command(
@@ -545,6 +549,7 @@ async def take_screenshot(
545549
self,
546550
path: Optional[str] = None,
547551
quality: int = 100,
552+
beyond_viewport: bool = False,
548553
as_base64: bool = False,
549554
) -> Optional[str]:
550555
"""
@@ -553,6 +558,8 @@ async def take_screenshot(
553558
Args:
554559
path: File path for screenshot (extension determines format).
555560
quality: Image quality 0-100 (default 100).
561+
beyond_viewport: The page will be scrolled to the bottom and the screenshot will
562+
include the entire page
556563
as_base64: Return as base64 string instead of saving file.
557564
558565
Returns:
@@ -573,6 +580,7 @@ async def take_screenshot(
573580
PageCommands.capture_screenshot(
574581
format=ScreenshotFormat.get_value(output_extension),
575582
quality=quality,
583+
capture_beyond_viewport=beyond_viewport,
576584
)
577585
)
578586
screenshot_data = response['result']['data']

pydoll/elements/web_element.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ async def get_bounds_using_js(self) -> dict[str, int]:
130130
131131
Returns coordinates relative to viewport (alternative to bounds property).
132132
"""
133-
response = await self._execute_script(Scripts.BOUNDS, return_by_value=True)
133+
response = await self.execute_script(Scripts.BOUNDS, return_by_value=True)
134134
return json.loads(response['result']['result']['value'])
135135

136136
async def get_parent_element(self) -> 'WebElement':
137137
"""Element's parent element."""
138-
result = await self._execute_script(Scripts.GET_PARENT_NODE)
138+
result = await self.execute_script(Scripts.GET_PARENT_NODE)
139139
if not self._has_object_id_key(result):
140140
raise ElementNotFound(f'Parent element not found for element: {self}')
141141

@@ -195,14 +195,12 @@ async def wait_until(
195195
WaitElementTimeout: If the condition is not met within ``timeout``.
196196
"""
197197
checks_map = [
198-
(is_visible, self._is_element_visible),
199-
(is_interactable, self._is_element_interactable),
198+
(is_visible, self.is_visible),
199+
(is_interactable, self.is_interactable),
200200
]
201201
checks = [func for flag, func in checks_map if flag]
202202
if not checks:
203-
raise ValueError(
204-
'At least one of is_visible or is_interactable must be True'
205-
)
203+
raise ValueError('At least one of is_visible or is_interactable must be True')
206204

207205
condition_parts = []
208206
if is_visible:
@@ -219,9 +217,7 @@ async def wait_until(
219217
return
220218

221219
if timeout and loop.time() - start_time > timeout:
222-
raise WaitElementTimeout(
223-
f'Timed out waiting for element to become {condition_msg}'
224-
)
220+
raise WaitElementTimeout(f'Timed out waiting for element to become {condition_msg}')
225221

226222
await asyncio.sleep(0.5)
227223

@@ -242,10 +238,10 @@ async def click_using_js(self):
242238

243239
await self.scroll_into_view()
244240

245-
if not await self._is_element_visible():
241+
if not await self.is_visible():
246242
raise ElementNotVisible()
247243

248-
result = await self._execute_script(Scripts.CLICK, return_by_value=True)
244+
result = await self.execute_script(Scripts.CLICK, return_by_value=True)
249245
clicked = result['result']['result']['value']
250246
if not clicked:
251247
raise ElementNotInteractable()
@@ -274,7 +270,7 @@ async def click(
274270
if self._is_option_tag():
275271
return await self._click_option_tag()
276272

277-
if not await self._is_element_visible():
273+
if not await self.is_visible():
278274
raise ElementNotVisible()
279275

280276
await self.scroll_into_view()
@@ -410,24 +406,22 @@ async def _click_option_tag(self):
410406
)
411407
)
412408

413-
async def _is_element_visible(self):
409+
async def is_visible(self):
414410
"""Check if element is visible using comprehensive JavaScript visibility test."""
415-
result = await self._execute_script(Scripts.ELEMENT_VISIBLE, return_by_value=True)
411+
result = await self.execute_script(Scripts.ELEMENT_VISIBLE, return_by_value=True)
416412
return result['result']['result']['value']
417413

418-
async def _is_element_on_top(self):
414+
async def is_on_top(self):
419415
"""Check if element is topmost at its center point (not covered by overlays)."""
420-
result = await self._execute_script(Scripts.ELEMENT_ON_TOP, return_by_value=True)
416+
result = await self.execute_script(Scripts.ELEMENT_ON_TOP, return_by_value=True)
421417
return result['result']['result']['value']
422418

423-
async def _is_element_interactable(self):
419+
async def is_interactable(self):
424420
"""Check if element is interactable based on visibility and position."""
425-
result = await self._execute_script(
426-
Scripts.ELEMENT_INTERACTIVE, return_by_value=True
427-
)
421+
result = await self.execute_script(Scripts.ELEMENT_INTERACTIVE, return_by_value=True)
428422
return result['result']['result']['value']
429423

430-
async def _execute_script(self, script: str, return_by_value: bool = False):
424+
async def execute_script(self, script: str, return_by_value: bool = False):
431425
"""
432426
Execute JavaScript in element context.
433427

pydoll/protocol/browser/methods.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -203,26 +203,26 @@ class GetWindowForTargetResult(TypedDict):
203203

204204
# Command types
205205
AddPrivacySandboxCoordinatorKeyConfigCommand = Command[
206-
AddPrivacySandboxCoordinatorKeyConfigParams, EmptyResponse
206+
AddPrivacySandboxCoordinatorKeyConfigParams, Response[EmptyResponse]
207207
]
208208
AddPrivacySandboxEnrollmentOverrideCommand = Command[
209-
AddPrivacySandboxEnrollmentOverrideParams, EmptyResponse
209+
AddPrivacySandboxEnrollmentOverrideParams, Response[EmptyResponse]
210210
]
211-
CancelDownloadCommand = Command[CancelDownloadParams, EmptyResponse]
212-
CloseCommand = Command[EmptyParams, EmptyResponse]
213-
CrashCommand = Command[EmptyParams, EmptyResponse]
214-
CrashGpuProcessCommand = Command[EmptyParams, EmptyResponse]
215-
ExecuteBrowserCommandCommand = Command[ExecuteBrowserCommandParams, EmptyResponse]
211+
CancelDownloadCommand = Command[CancelDownloadParams, Response[EmptyResponse]]
212+
CloseCommand = Command[EmptyParams, Response[EmptyResponse]]
213+
CrashCommand = Command[EmptyParams, Response[EmptyResponse]]
214+
CrashGpuProcessCommand = Command[EmptyParams, Response[EmptyResponse]]
215+
ExecuteBrowserCommandCommand = Command[ExecuteBrowserCommandParams, Response[EmptyResponse]]
216216
GetBrowserCommandLineCommand = Command[EmptyParams, GetBrowserCommandLineResponse]
217217
GetHistogramCommand = Command[GetHistogramParams, GetHistogramResponse]
218218
GetHistogramsCommand = Command[GetHistogramsParams, GetHistogramsResponse]
219219
GetVersionCommand = Command[EmptyParams, GetVersionResponse]
220220
GetWindowBoundsCommand = Command[GetWindowBoundsParams, GetWindowBoundsResponse]
221221
GetWindowForTargetCommand = Command[GetWindowForTargetParams, GetWindowForTargetResponse]
222-
GrantPermissionsCommand = Command[GrantPermissionsParams, EmptyResponse]
223-
ResetPermissionsCommand = Command[ResetPermissionsParams, EmptyResponse]
224-
SetContentsSizeCommand = Command[SetContentsSizeParams, EmptyResponse]
225-
SetDockTileCommand = Command[SetDockTileParams, EmptyResponse]
226-
SetDownloadBehaviorCommand = Command[SetDownloadBehaviorParams, EmptyResponse]
227-
SetPermissionCommand = Command[SetPermissionParams, EmptyResponse]
228-
SetWindowBoundsCommand = Command[SetWindowBoundsParams, EmptyResponse]
222+
GrantPermissionsCommand = Command[GrantPermissionsParams, Response[EmptyResponse]]
223+
ResetPermissionsCommand = Command[ResetPermissionsParams, Response[EmptyResponse]]
224+
SetContentsSizeCommand = Command[SetContentsSizeParams, Response[EmptyResponse]]
225+
SetDockTileCommand = Command[SetDockTileParams, Response[EmptyResponse]]
226+
SetDownloadBehaviorCommand = Command[SetDownloadBehaviorParams, Response[EmptyResponse]]
227+
SetPermissionCommand = Command[SetPermissionParams, Response[EmptyResponse]]
228+
SetWindowBoundsCommand = Command[SetWindowBoundsParams, Response[EmptyResponse]]

0 commit comments

Comments
 (0)