Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions public/docs/deep-dive/tab-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,21 @@ These visual capture capabilities are invaluable for:
- Debugging automation scripts
- Archiving page content

!!! warning "Top-level targets vs iFrames for Tab screenshots"
`Tab.take_screenshot()` relies on CDP's `Page.captureScreenshot`, which only works for top-level targets. If you obtained a `Tab` for an iframe using `await tab.get_frame(iframe_element)`, calling `take_screenshot()` on that iframe tab will raise `TopLevelTargetRequired`.

Use `WebElement.take_screenshot()` inside iframes. It captures via the viewport and works within the iframe context.

```python
# Wrong: iframe Tab screenshot (raises TopLevelTargetRequired)
iframe_tab = await tab.get_frame(iframe_element)
await iframe_tab.take_screenshot(as_base64=True) # will raise an exception

# Correct: element screenshot inside iframe (uses viewport)
element = await iframe_tab.find(id='captcha')
await element.take_screenshot('captcha.png') # will work!
```

## Event System Overview

The Tab domain provides a comprehensive event system for monitoring and reacting to browser events:
Expand Down
3 changes: 3 additions & 0 deletions public/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Capture visual content from web pages:
- **High-Quality PDF Export**: Generate PDF documents from web pages
- **Custom Formatting**: Coming soon!

!!! note "Screenshots in iFrames and top-level targets"
`tab.take_screenshot()` only works on top-level targets. When working inside an `iframe` (using `await tab.get_frame(iframe_element)`), Chrome's `Page.captureScreenshot` cannot capture the subtarget directly. In these scenarios, use `WebElement.take_screenshot()` instead—it captures via viewport and works inside iframes.

## Remote Connections and Hybrid Automation

### Connect to a running browser via WebSocket
Expand Down
15 changes: 15 additions & 0 deletions public/docs/zh/deep-dive/tab-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,21 @@ await tab.print_to_pdf(
- 调试自动化脚本
- 存档页面内容

!!! 警告 "顶层目标与 iframe 的截图差异"
`Tab.take_screenshot()` 依赖 CDP 的 `Page.captureScreenshot`,该能力仅适用于顶层目标(top-level target)。如果通过 `await tab.get_frame(iframe_element)` 获取了 iframe 对应的 `Tab`,在此 `Tab` 上调用 `take_screenshot()` 会抛出 `TopLevelTargetRequired`。

在 iframe 内请使用 `WebElement.take_screenshot()`。它基于视口(viewport)进行捕获,适用于 iframe 场景。

```python
# 错误:在 iframe Tab 上截图(会抛出 TopLevelTargetRequired)
iframe_tab = await tab.get_frame(iframe_element)
await iframe_tab.take_screenshot(as_base64=True) # 会抛出异常

# 正确:在 iframe 内对元素截图(基于视口)
element = await iframe_tab.find(id='captcha')
await element.take_screenshot('captcha.png') # 会正常工作!
```

## 事件系统概述

Tab 域提供了一个全面的事件系统,用于监控和响应浏览器事件:
Expand Down
3 changes: 3 additions & 0 deletions public/docs/zh/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Pydoll支持操作任何Chromium核心的浏览器:
- **高质量 PDF 导出**:从网页生成 PDF 文档
- **自定义格式**:即将推出!

!!! 信息 "关于 iframe 与顶层目标的截图"
`tab.take_screenshot()` 仅适用于顶层目标(top-level target)。在 `iframe` 内(通过 `await tab.get_frame(iframe_element)` 获取的子目标)时,Chrome 的 `Page.captureScreenshot` 无法直接对该子目标截图。这种情况下请改用 `WebElement.take_screenshot()`,它基于视口(viewport)进行捕获,适用于 iframe 内部。

## 远程连接与混合自动化

### 通过 WebSocket 连接已运行的浏览器
Expand Down
13 changes: 11 additions & 2 deletions pydoll/browser/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
NoDialogPresent,
NotAnIFrame,
PageLoadTimeout,
TopLevelTargetRequired,
WaitElementTimeout,
)
from pydoll.protocol.base import EmptyResponse, Response
Expand Down Expand Up @@ -252,7 +253,7 @@ async def enable_intercept_file_chooser_dialog(self):
async def enable_auto_solve_cloudflare_captcha(
self,
custom_selector: Optional[tuple[By, str]] = None,
time_before_click: int = 2,
time_before_click: int = 5,
time_to_wait_captcha: int = 5,
):
"""
Expand Down Expand Up @@ -527,7 +528,15 @@ async def take_screenshot(
capture_beyond_viewport=beyond_viewport,
)
)
screenshot_data = response['result']['data']

try:
screenshot_data = response['result']['data']
except KeyError:
raise TopLevelTargetRequired(
'Command can only be executed on top-level targets. Please use '
'take_screenshot method on the WebElement object instead.'
)

if as_base64:
return screenshot_data

Expand Down
10 changes: 5 additions & 5 deletions pydoll/elements/mixins/find_elements_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async def find(
timeout: int = ...,
find_all: Literal[False] = False,
raise_exc: Literal[True] = True,
**attributes: dict[str, str],
**attributes,
) -> 'WebElement': ...

@overload
Expand All @@ -73,7 +73,7 @@ async def find(
timeout: int = ...,
find_all: Literal[True] = True,
raise_exc: Literal[True] = True,
**attributes: dict[str, str],
**attributes,
) -> list['WebElement']: ...

@overload
Expand All @@ -87,7 +87,7 @@ async def find(
timeout: int = ...,
find_all: Literal[True] = True,
raise_exc: Literal[False] = False,
**attributes: dict[str, str],
**attributes,
) -> Optional[list['WebElement']]: ...

@overload
Expand All @@ -101,7 +101,7 @@ async def find(
timeout: int = ...,
find_all: Literal[False] = False,
raise_exc: Literal[False] = False,
**attributes: dict[str, str],
**attributes,
) -> Optional['WebElement']: ...

@overload
Expand All @@ -115,7 +115,7 @@ async def find(
timeout: int = ...,
find_all: bool = ...,
raise_exc: bool = ...,
**attributes: dict[str, str],
**attributes,
) -> Union['WebElement', list['WebElement'], None]: ...

async def find(
Expand Down
6 changes: 6 additions & 0 deletions pydoll/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ class ProtocolException(PydollException):
message = 'A protocol error occurred'


class TopLevelTargetRequired(ProtocolException):
"""Raised when a command can only be executed on top-level targets."""

message = 'Command can only be executed on top-level targets.'


class InvalidCommand(ProtocolException):
"""Raised when an invalid command is sent to the browser."""

Expand Down
9 changes: 9 additions & 0 deletions tests/test_browser/test_browser_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
WaitElementTimeout,
NetworkEventsNotEnabled,
InvalidScriptWithElement,
TopLevelTargetRequired,
)

@pytest_asyncio.fixture
Expand Down Expand Up @@ -424,6 +425,14 @@ async def test_take_screenshot_beyond_viewport(self, tab):
assert command['params']['captureBeyondViewport'] is True
assert result == screenshot_data

@pytest.mark.asyncio
async def test_take_screenshot_in_iframe_raises_top_level_required(self, tab):
"""Tab.take_screenshot must be called on top-level targets; iframe Tab raises."""
# Simulate CDP returning no image data (missing 'data' key) for non top-level target
with patch.object(tab, '_execute_command', AsyncMock(return_value={'result': {}})):
with pytest.raises(TopLevelTargetRequired):
await tab.take_screenshot(path=None, as_base64=True)

@pytest.mark.asyncio
async def test_print_to_pdf_to_file(self, tab, tmp_path):
"""Test printing to PDF and saving to file."""
Expand Down
Loading