diff --git a/.gitignore b/.gitignore index 6e36e11..0aa31d4 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,6 @@ venv.bak/ # VisualStudioCode .vscode .history + +# Pycharm +/.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e2e97..3fb006b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `tab.expect_request` and `tab.expect_response` methods to wait for a specific request or response @3mora2 +- Add `tab.wait_for_ready_state` method for to wait for page to load @3mora2 - Add `tab.remove_handlers` method for removing handlers @khamaileon - Clean up temporary profiles when `Browser.stop()` is called @barrycarey diff --git a/examples/wait_for_page.py b/examples/wait_for_page.py new file mode 100644 index 0000000..b15ed15 --- /dev/null +++ b/examples/wait_for_page.py @@ -0,0 +1,23 @@ +import asyncio +import zendriver as zd + + +async def main(): + async with await zd.start() as browser: + tab = browser.main_tab + async with tab.expect_request("https://github.com/") as request_info: + async with tab.expect_response( + "https://github.githubassets.com/assets/.*" + ) as response_info: + await tab.get("https://github.com/") + await tab.wait_for_ready_state(until="complete") + + req = await request_info.value + print(req.request_id) + + res = await response_info.value + print(res.request_id) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/zendriver/core/tab.py b/zendriver/core/tab.py index 9f511f7..c4d0b2e 100644 --- a/zendriver/core/tab.py +++ b/zendriver/core/tab.py @@ -4,11 +4,12 @@ import datetime import logging import pathlib +import re import typing import urllib.parse import warnings import webbrowser -from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, Literal from .. import cdp from . import element, util @@ -1125,6 +1126,36 @@ async def wait_for( raise asyncio.TimeoutError("time ran out while waiting") + async def wait_for_ready_state( + self, + until: Literal["loading", "interactive", "complete"] = "interactive", + timeout=30, + ): + loop = asyncio.get_event_loop() + start_time = loop.time() + + while True: + ready_state = await self.evaluate("document.readyState") + if ready_state == until: + return True + + if loop.time() - start_time > timeout: + raise asyncio.TimeoutError( + "time ran out while waiting for load page until %s" % until + ) + + await asyncio.sleep(0.1) + + def expect_request( + self, url_pattern: Union[str, re.Pattern[str]] + ) -> "RequestExpectation": + return RequestExpectation(self, url_pattern) + + def expect_response( + self, url_pattern: Union[str, re.Pattern[str]] + ) -> "ResponseExpectation": + return ResponseExpectation(self, url_pattern) + async def download_file(self, url: str, filename: Optional[PathLike] = None): """ downloads file by given url. @@ -1446,3 +1477,68 @@ def __repr__(self): extra = f"[url: {self.target.url}]" s = f"<{type(self).__name__} [{self.target_id}] [{self.type_}] {extra}>" return s + + +class BaseRequestExpectation: + def __init__(self, tab: Tab, url_pattern: Union[str, re.Pattern[str]]): + self.tab = tab + self.url_pattern = url_pattern + self.request_future: asyncio.Future[cdp.network.RequestWillBeSent] = ( + asyncio.Future() + ) + self.response_future: asyncio.Future[cdp.network.ResponseReceived] = ( + asyncio.Future() + ) + self.request_id: Union[cdp.network.RequestId, None] = None + + async def _request_handler(self, event: cdp.network.RequestWillBeSent): + if re.fullmatch(self.url_pattern, event.request.url): + self._remove_request_handler() + self.request_id = event.request_id + self.request_future.set_result(event) + + async def _response_handler(self, event: cdp.network.ResponseReceived): + if event.request_id == self.request_id: + self._remove_response_handler() + self.response_future.set_result(event) + + def _remove_request_handler(self): + self.tab.remove_handlers(cdp.network.RequestWillBeSent, self._request_handler) + + def _remove_response_handler(self): + self.tab.remove_handlers(cdp.network.ResponseReceived, self._response_handler) + + async def __aenter__(self): + self.tab.add_handler(cdp.network.RequestWillBeSent, self._request_handler) + self.tab.add_handler(cdp.network.ResponseReceived, self._response_handler) + return self + + async def __aexit__(self, *args): + self._remove_request_handler() + self._remove_response_handler() + + @property + async def request(self): + return (await self.request_future).request + + @property + async def response(self): + return (await self.response_future).response + + @property + async def response_body(self): + request_id = (await self.request_future).request_id + body = await self.tab.send(cdp.network.get_response_body(request_id=request_id)) + return body + + +class RequestExpectation(BaseRequestExpectation): + @property + async def value(self) -> cdp.network.RequestWillBeSent: + return await self.request_future + + +class ResponseExpectation(BaseRequestExpectation): + @property + async def value(self) -> cdp.network.ResponseReceived: + return await self.response_future