diff --git a/public/docs/deep-dive/tab-domain.md b/public/docs/deep-dive/tab-domain.md index 1b304b38..630a30af 100644 --- a/public/docs/deep-dive/tab-domain.md +++ b/public/docs/deep-dive/tab-domain.md @@ -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: diff --git a/public/docs/features.md b/public/docs/features.md index 0e6497b2..acac8183 100644 --- a/public/docs/features.md +++ b/public/docs/features.md @@ -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 diff --git a/public/docs/zh/deep-dive/tab-domain.md b/public/docs/zh/deep-dive/tab-domain.md index f552f78d..c338e937 100644 --- a/public/docs/zh/deep-dive/tab-domain.md +++ b/public/docs/zh/deep-dive/tab-domain.md @@ -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 域提供了一个全面的事件系统,用于监控和响应浏览器事件: diff --git a/public/docs/zh/features.md b/public/docs/zh/features.md index 0d2d6f59..3653f2f1 100644 --- a/public/docs/zh/features.md +++ b/public/docs/zh/features.md @@ -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 连接已运行的浏览器 diff --git a/pydoll/browser/tab.py b/pydoll/browser/tab.py index aa18be2c..3c11116c 100644 --- a/pydoll/browser/tab.py +++ b/pydoll/browser/tab.py @@ -46,6 +46,7 @@ NoDialogPresent, NotAnIFrame, PageLoadTimeout, + TopLevelTargetRequired, WaitElementTimeout, ) from pydoll.protocol.base import EmptyResponse, Response @@ -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, ): """ @@ -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 diff --git a/pydoll/elements/mixins/find_elements_mixin.py b/pydoll/elements/mixins/find_elements_mixin.py index 8adaf742..91cd6b38 100644 --- a/pydoll/elements/mixins/find_elements_mixin.py +++ b/pydoll/elements/mixins/find_elements_mixin.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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( diff --git a/pydoll/exceptions.py b/pydoll/exceptions.py index 08e8e409..f4746230 100644 --- a/pydoll/exceptions.py +++ b/pydoll/exceptions.py @@ -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.""" diff --git a/tests/test_browser/test_browser_tab.py b/tests/test_browser/test_browser_tab.py index 515e499a..d443b085 100644 --- a/tests/test_browser/test_browser_tab.py +++ b/tests/test_browser/test_browser_tab.py @@ -23,6 +23,7 @@ WaitElementTimeout, NetworkEventsNotEnabled, InvalidScriptWithElement, + TopLevelTargetRequired, ) @pytest_asyncio.fixture @@ -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."""