From 797946e5fc74825d6bec80ec2e59689aed7b7c8e Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 30 Apr 2026 15:26:52 +0200 Subject: [PATCH 01/22] fix: support async predicates in page.expect_request/expect_response (#3055) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 4 +++ playwright/_impl/_helper.py | 9 ++++-- playwright/_impl/_page.py | 5 +-- playwright/_impl/_waiter.py | 44 ++++++++++++++++++++++++--- playwright/async_api/_generated.py | 12 +++++--- scripts/documentation_provider.py | 10 ++++++ scripts/expected_api_mismatch.txt | 4 +++ scripts/generate_api.py | 11 +++++++ scripts/generate_sync_api.py | 3 ++ tests/async/test_page.py | 49 +++++++++++++++++++++++++++++- 10 files changed, 137 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ce4ec7c07..4153ac4d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,10 @@ This is the recurring high-stakes task. Use the dedicated skill: It documents the full process: the upstream commit-range diff over `docs/src/api/`, how to classify each commit (PORT / MISMATCH / N/A), how to handle the `langs:` filter, the recurring failure modes, and the tests/sync-mirroring conventions. +## Working on PRs + +- Never post comments, replies, or reviews on GitHub PRs/issues under my account without my explicit approval. Draft the proposed text and wait for me to approve before sending. + ## House style - Don't hand-edit generated files. diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 1d7e4f67b..dc0a2479d 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -22,6 +22,7 @@ from typing import ( TYPE_CHECKING, Any, + Awaitable, Callable, Dict, List, @@ -54,8 +55,12 @@ from playwright._impl._network import Request, Response, Route, WebSocketRoute URLMatch = Union[str, Pattern[str], Callable[[str], bool]] -URLMatchRequest = Union[str, Pattern[str], Callable[["Request"], bool]] -URLMatchResponse = Union[str, Pattern[str], Callable[["Response"], bool]] +URLMatchRequest = Union[ + str, Pattern[str], Callable[["Request"], Union[bool, Awaitable[bool]]] +] +URLMatchResponse = Union[ + str, Pattern[str], Callable[["Response"], Union[bool, Awaitable[bool]]] +] RouteHandlerCallback = Union[ Callable[["Route"], Any], Callable[["Route", "Request"], Any] ] diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 020058acf..5a8444624 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -22,6 +22,7 @@ from typing import ( TYPE_CHECKING, Any, + Awaitable, Callable, Dict, List, @@ -1278,7 +1279,7 @@ def expect_request( urlOrPredicate: URLMatchRequest, timeout: float = None, ) -> EventContextManagerImpl[Request]: - def my_predicate(request: Request) -> bool: + def my_predicate(request: Request) -> Union[bool, Awaitable[bool]]: if not callable(urlOrPredicate): return url_matches( self._browser_context._base_url, @@ -1310,7 +1311,7 @@ def expect_response( urlOrPredicate: URLMatchResponse, timeout: float = None, ) -> EventContextManagerImpl[Response]: - def my_predicate(request: Response) -> bool: + def my_predicate(request: Response) -> Union[bool, Awaitable[bool]]: if not callable(urlOrPredicate): return url_matches( self._browser_context._base_url, diff --git a/playwright/_impl/_waiter.py b/playwright/_impl/_waiter.py index f7ff4b6c1..b5bf53382 100644 --- a/playwright/_impl/_waiter.py +++ b/playwright/_impl/_waiter.py @@ -13,10 +13,11 @@ # limitations under the License. import asyncio +import inspect import math import uuid from asyncio.tasks import Task -from typing import Any, Callable, List, Tuple, Union +from typing import Any, Callable, List, Optional, Tuple, Union from pyee import EventEmitter @@ -71,9 +72,11 @@ def reject_on_event( error: Union[Error, Callable[..., Error]], predicate: Callable = None, ) -> None: + def on_match() -> None: + self._reject(error() if callable(error) else error) + def listener(event_data: Any = None) -> None: - if not predicate or predicate(event_data): - self._reject(error() if callable(error) else error) + self._evaluate_predicate(predicate, event_data, on_match) emitter.on(event, listener) self._registered_listeners.append((emitter, event, listener)) @@ -117,12 +120,43 @@ def wait_for_event( predicate: Callable = None, ) -> None: def listener(event_data: Any = None) -> None: - if not predicate or predicate(event_data): - self._fulfill(event_data) + self._evaluate_predicate( + predicate, event_data, lambda: self._fulfill(event_data) + ) emitter.on(event, listener) self._registered_listeners.append((emitter, event, listener)) + def _evaluate_predicate( + self, + predicate: Optional[Callable], + event_data: Any, + on_match: Callable[[], None], + ) -> None: + if predicate is None: + on_match() + return + try: + result = predicate(event_data) + except Exception as e: + self._reject(e) + return + if inspect.iscoroutine(result): + + async def _await_predicate(coro: Any) -> None: + try: + matched = await coro + except Exception as e: + self._reject(e) + return + if matched and not self._result.done(): + on_match() + + self._pending_tasks.append(self._loop.create_task(_await_predicate(result))) + return + if result: + on_match() + def result(self) -> asyncio.Future: return self._result diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 5ca533ef2..130230390 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -12306,7 +12306,9 @@ def expect_popup( def expect_request( self, url_or_predicate: typing.Union[ - str, typing.Pattern[str], typing.Callable[["Request"], bool] + str, + typing.Pattern[str], + typing.Callable[["Request"], typing.Union[bool, typing.Awaitable[bool]]], ], *, timeout: typing.Optional[float] = None, @@ -12331,7 +12333,7 @@ def expect_request( Parameters ---------- - url_or_predicate : Union[Callable[[Request], bool], Pattern[str], str] + url_or_predicate : Union[Callable[[Request], Union[bool, typing.Awaitable[bool]]], Pattern[str], str] Request URL string, regex or predicate receiving `Request` object. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. @@ -12384,7 +12386,9 @@ def expect_request_finished( def expect_response( self, url_or_predicate: typing.Union[ - str, typing.Pattern[str], typing.Callable[["Response"], bool] + str, + typing.Pattern[str], + typing.Callable[["Response"], typing.Union[bool, typing.Awaitable[bool]]], ], *, timeout: typing.Optional[float] = None, @@ -12411,7 +12415,7 @@ def expect_response( Parameters ---------- - url_or_predicate : Union[Callable[[Response], bool], Pattern[str], str] + url_or_predicate : Union[Callable[[Response], Union[bool, typing.Awaitable[bool]]], Pattern[str], str] Request URL string, regex or predicate receiving `Response` object. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 8016a601d..4c2d9b95d 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -408,6 +408,16 @@ def serialize_python_type(self, value: Any, direction: str) -> str: return f"{{{', '.join(signature)}}}" if origin == Union: args = get_args(value) + if not self.is_async: + # Sync API doesn't accept awaitable callbacks; drop the + # Awaitable arm so docstring types match the sync signature. + args = tuple( + a + for a in args + if str(get_origin(a)) != "" + ) + if len(args) == 1: + return self.serialize_python_type(args[0], direction) if len(args) == 2 and str(args[1]) == "": return self.make_optional( self.serialize_python_type(args[0], direction) diff --git a/scripts/expected_api_mismatch.txt b/scripts/expected_api_mismatch.txt index cddb87f15..d9bcc41a0 100644 --- a/scripts/expected_api_mismatch.txt +++ b/scripts/expected_api_mismatch.txt @@ -20,3 +20,7 @@ Parameter type mismatch in BrowserContext.route_web_socket(handler=): documented Parameter type mismatch in Page.route_web_socket(handler=): documented as Callable[[WebSocketRoute], Union[Any, Any]], code has Callable[[WebSocketRoute], Any] Parameter type mismatch in WebSocketRoute.on_close(handler=): documented as Callable[[Union[int, undefined]], Union[Any, Any]], code has Callable[[Union[int, None], Union[str, None]], Any] Parameter type mismatch in WebSocketRoute.on_message(handler=): documented as Callable[[str], Union[Any, Any]], code has Callable[[Union[bytes, str]], Any] + +# Async API additionally accepts an `async def` predicate. +Parameter type mismatch in Page.expect_request(url_or_predicate=): documented as Union[Callable[[Request], bool], Pattern[str], str], code has Union[Callable[[Request], Union[bool, typing.Awaitable[bool]]], Pattern[str], str] +Parameter type mismatch in Page.expect_response(url_or_predicate=): documented as Union[Callable[[Response], bool], Pattern[str], str], code has Union[Callable[[Response], Union[bool, typing.Awaitable[bool]]], Pattern[str], str] diff --git a/scripts/generate_api.py b/scripts/generate_api.py index 9d217b7c5..07a1f2182 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -56,6 +56,8 @@ from playwright._impl._video import Video from playwright._impl._web_error import WebError +SYNC_API = False + def process_type(value: Any, param: bool = False) -> str: value = str(value) @@ -65,6 +67,15 @@ def process_type(value: Any, param: bool = False) -> str: value = re.sub(r"playwright\._impl\._api_structures.([\w]+)", r"\1", value) value = re.sub(r"playwright\._impl\.[\w]+\.([\w]+)", r'"\1"', value) value = re.sub(r"typing.Literal", "Literal", value) + if SYNC_API: + # Sync API does not accept awaitable callbacks; collapse + # Union[X, Awaitable[X]] (used for predicates the async API also + # accepts as `async def`) down to just X. + value = re.sub( + r"typing\.Union\[([^\[\],]+),\s*typing\.Awaitable\[\1\]\]", + r"\1", + value, + ) if param: value = re.sub(r"^typing.Union\[([^,]+), None\]$", r"\1 = None", value) value = re.sub( diff --git a/scripts/generate_sync_api.py b/scripts/generate_sync_api.py index 5ccf3b672..dbabce413 100755 --- a/scripts/generate_sync_api.py +++ b/scripts/generate_sync_api.py @@ -19,6 +19,7 @@ from types import FunctionType from typing import Any +import generate_api from documentation_provider import DocumentationProvider from generate_api import ( api_globals, @@ -33,6 +34,8 @@ signature, ) +generate_api.SYNC_API = True + documentation_provider = DocumentationProvider(False) diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 562d98248..2fcf4c92d 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -16,7 +16,7 @@ import os import re from pathlib import Path -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import pytest @@ -352,6 +352,53 @@ async def test_wait_for_response_should_work_with_predicate( assert response.url == server.PREFIX + "/digits/2.png" +async def test_wait_for_response_should_work_with_async_predicate( + page: Page, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + + async def predicate(response: Any) -> bool: + await asyncio.sleep(0) + return response.url == server.PREFIX + "/digits/2.png" + + async with page.expect_response(predicate) as response_info: + await page.evaluate( + """() => { + fetch('/digits/1.png') + fetch('/digits/2.png') + fetch('/digits/3.png') + }""" + ) + response = await response_info.value + assert response.url == server.PREFIX + "/digits/2.png" + + +async def test_expect_response_should_reject_when_async_predicate_throws( + page: Page, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + + async def predicate(response: Any) -> bool: + raise Exception("Async oops!") + + with pytest.raises(Exception, match="Async oops!"): + async with page.expect_response(predicate): + await page.evaluate("() => fetch('/digits/1.png')") + + +async def test_expect_response_should_reject_when_sync_predicate_throws( + page: Page, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + + def predicate(response: Any) -> bool: + raise Exception("Sync oops!") + + with pytest.raises(Exception, match="Sync oops!"): + async with page.expect_response(predicate): + await page.evaluate("() => fetch('/digits/1.png')") + + async def test_wait_for_response_should_work_with_no_timeout( page: Page, server: Server ) -> None: From 67e59f5804411f59a5580d0dd7796fd525c59742 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 30 Apr 2026 15:54:11 +0100 Subject: [PATCH 02/22] fix(asyncio): do not deadlock in atexit handler (#3056) --- CLAUDE.md | 37 ++++++++++++++++++++ local-requirements.txt | 1 + playwright/_impl/_transport.py | 64 +++++++++++++++++++--------------- tests/async/test_asyncio.py | 43 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4153ac4d1..1cbaf3e1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,3 +59,40 @@ It documents the full process: the upstream commit-range diff over `docs/src/api - New public methods on impl classes need a sync test mirror under `tests/sync/`. - Keep `expected_api_mismatch.txt` minimal — every entry needs a one-line rationale comment above it. - Prefer `locals_to_params(locals())` for forwarding optional kwargs to channel sends, matching the rest of the codebase. + +## Commit Convention + +Before committing, run `mypy playwright` and fix errors. + +Semantic commit messages: `label(scope): description` + +Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops` + +```bash +git checkout -b fix-12345 +# ... make changes ... +git add +git commit -m "$(cat <<'EOF' +fix(asyncio): do not deadlock in atexit handler + +Fixes: https://github.com/microsoft/playwright-python/issues/12345 +EOF +)" +git push origin fix-12345 +gh pr create --repo microsoft/playwright-python --head username:fix-12345 \ + --title "fix(asyncio): do not deadlock in atexit handler" \ + --body "$(cat <<'EOF' +## Summary +- + +Fixes https://github.com/microsoft/playwright-python/issues/12345 +EOF +)" +``` + +Never add Co-Authored-By agents in commit message. +Never add "Generated with" in commit message. +Never add test plan to PR description. Keep PR description short — a few bullet points at most. +Branch naming for issue fixes: `fix-` + +**Never `git push` without an explicit instruction to push.** Applies even when a PR is already open for the branch — additional commits are immediately visible to reviewers. Commit locally, report what was committed, and wait. Only push when the user's message contains "push", "upload", "create PR", "ship it", or equivalent. diff --git a/local-requirements.txt b/local-requirements.txt index 8a72b5745..5735ae2a0 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -1,3 +1,4 @@ +asyncio-atexit==1.0.1 autobahn==23.1.2 black==25.1.0 build==1.3.0 diff --git a/playwright/_impl/_transport.py b/playwright/_impl/_transport.py index 2ca84d459..3cc029e18 100644 --- a/playwright/_impl/_transport.py +++ b/playwright/_impl/_transport.py @@ -137,38 +137,44 @@ async def connect(self) -> None: async def run(self) -> None: assert self._proc.stdout assert self._proc.stdin - while not self._stopped: - try: - buffer = await self._proc.stdout.readexactly(4) - if self._stopped: - break - length = int.from_bytes(buffer, byteorder="little", signed=False) - buffer = bytes(0) - while length: - to_read = min(length, 32768) - data = await self._proc.stdout.readexactly(to_read) + try: + while not self._stopped: + try: + buffer = await self._proc.stdout.readexactly(4) + if self._stopped: + break + length = int.from_bytes(buffer, byteorder="little", signed=False) + buffer = bytes(0) + while length: + to_read = min(length, 32768) + data = await self._proc.stdout.readexactly(to_read) + if self._stopped: + break + length -= to_read + if len(buffer): + buffer = buffer + data + else: + buffer = data if self._stopped: break - length -= to_read - if len(buffer): - buffer = buffer + data - else: - buffer = data - if self._stopped: - break - obj = self.deserialize_message(buffer) - self.on_message(obj) - except asyncio.IncompleteReadError: - if not self._stopped: - self.on_error_future.set_exception( - Exception("Connection closed while reading from the driver") - ) - break - await asyncio.sleep(0) - - await self._proc.communicate() - self._stopped_future.set_result(None) + obj = self.deserialize_message(buffer) + self.on_message(obj) + except asyncio.IncompleteReadError: + if not self._stopped: + self.on_error_future.set_exception( + Exception("Connection closed while reading from the driver") + ) + break + await asyncio.sleep(0) + + await self._proc.communicate() + finally: + # Release waiters on wait_until_stopped() even if this task was + # cancelled before reaching the end (e.g. by asyncio.run()'s + # task-cancellation phase that runs before asyncio-atexit hooks). + if not self._stopped_future.done(): + self._stopped_future.set_result(None) def send(self, message: Dict) -> None: assert self._output diff --git a/tests/async/test_asyncio.py b/tests/async/test_asyncio.py index 971c65473..c78648430 100644 --- a/tests/async/test_asyncio.py +++ b/tests/async/test_asyncio.py @@ -13,7 +13,10 @@ # limitations under the License. import asyncio import gc +import subprocess import sys +import textwrap +from pathlib import Path from typing import Dict import pytest @@ -89,6 +92,46 @@ async def raise_exception() -> None: assert await page.evaluate("() => 11 * 11") == 121 +def test_stop_does_not_deadlock_with_asyncio_atexit(tmp_path: Path) -> None: + # Regression test for https://github.com/microsoft/playwright-python/issues/3004. + # asyncio.run() cancels all remaining tasks (including transport.run()) before + # calling loop.close(). asyncio-atexit hooks loop.close() to run async cleanup, + # so awaiting playwright.stop() at that point used to deadlock on a future that + # the (already cancelled) run task would never set. + script = tmp_path / "atexit_stop.py" + script.write_text( + textwrap.dedent( + """ + import asyncio + + import asyncio_atexit + from playwright.async_api import async_playwright + + + async def main(): + pw = await async_playwright().start() + asyncio_atexit.register(lambda: stop(pw)) + + + async def stop(pw): + await pw.stop() + print("STOPPED", flush=True) + + + asyncio.run(main()) + """ + ) + ) + result = subprocess.run( + [sys.executable, str(script)], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0, result.stderr + assert "STOPPED" in result.stdout + + async def test_should_return_proper_api_name_on_error(page: Page) -> None: try: await page.evaluate("does_not_exist") From f3d8fd19aacae3fc6f44898aa335c25b6f97018a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 30 Apr 2026 18:27:34 +0200 Subject: [PATCH 03/22] fix: handle ignore_default_args=False in launch params (#3058) Co-authored-by: lp07 --- playwright/_impl/_browser_type.py | 2 ++ tests/async/test_launcher.py | 9 +++++++++ tests/sync/test_launcher.py | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index ba376c336..88ea5fc52 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -357,6 +357,8 @@ def normalize_launch_params(params: Dict) -> None: if params["ignoreDefaultArgs"] is True: params["ignoreAllDefaultArgs"] = True del params["ignoreDefaultArgs"] + elif params["ignoreDefaultArgs"] is False: + del params["ignoreDefaultArgs"] if "executablePath" in params: params["executablePath"] = str(Path(params["executablePath"])) if "downloadsPath" in params: diff --git a/tests/async/test_launcher.py b/tests/async/test_launcher.py index bd5dd82de..2b954fd61 100644 --- a/tests/async/test_launcher.py +++ b/tests/async/test_launcher.py @@ -107,3 +107,12 @@ async def test_browser_close_should_be_callable_twice( browser.close(), ) await browser.close() + + +async def test_browser_type_launch_should_accept_ignore_default_args_false( + browser_type: BrowserType, launch_arguments: Dict +) -> None: + # Regression for https://github.com/microsoft/playwright-python/issues/3005: + # passing False used to crash with "expected array, got boolean". + browser = await browser_type.launch(**launch_arguments, ignore_default_args=False) + await browser.close() diff --git a/tests/sync/test_launcher.py b/tests/sync/test_launcher.py index 2e5ec1573..6b89b79a8 100644 --- a/tests/sync/test_launcher.py +++ b/tests/sync/test_launcher.py @@ -88,3 +88,10 @@ def test_browser_close_should_be_callable_twice( browser = browser_type.launch(**launch_arguments) browser.close() browser.close() + + +def test_browser_type_launch_should_accept_ignore_default_args_false( + browser_type: BrowserType, launch_arguments: Dict +) -> None: + browser = browser_type.launch(**launch_arguments, ignore_default_args=False) + browser.close() From 86415b64388abbeda9bdd89ade8c4cc2c647cc8f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 30 Apr 2026 22:25:54 +0100 Subject: [PATCH 04/22] fix(typing): typed overloads for expect_event and wait_for_event (#3061) --- playwright/async_api/_generated.py | 729 ++++++++++++++++++++- playwright/sync_api/_generated.py | 729 ++++++++++++++++++++- scripts/documentation_provider.py | 49 ++ scripts/generate_async_api.py | 2 + scripts/generate_sync_api.py | 2 + tests/async/test_browsercontext_events.py | 2 +- tests/async/test_page_request_intercept.py | 3 +- tests/async/test_resource_timing.py | 5 +- tests/sync/test_resource_timing.py | 6 +- 9 files changed, 1489 insertions(+), 38 deletions(-) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 130230390..9e8737c12 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -1120,6 +1120,55 @@ def url(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.url) + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["WebSocket"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framereceived"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["typing.Union[bytes, str]"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framesent"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["typing.Union[bytes, str]"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["socketerror"], + predicate: typing.Optional[typing.Callable[["str"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["str"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -1153,6 +1202,55 @@ def expect_event( ).future ) + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebSocket": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["framereceived"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "typing.Union[bytes, str]": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["framesent"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "typing.Union[bytes, str]": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["socketerror"], + predicate: typing.Optional[typing.Callable[["str"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "str": ... + + @typing.overload + async def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + async def wait_for_event( self, event: str, @@ -6737,6 +6835,33 @@ async def evaluate_handle( ) ) + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["ConsoleMessage"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -9531,6 +9656,186 @@ async def wait_for_url( ) ) + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "ConsoleMessage": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["crash"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Dialog": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["domcontentloaded"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["download"], + predicate: typing.Optional[typing.Callable[["Download"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Download": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["filechooser"], + predicate: typing.Optional[typing.Callable[["FileChooser"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "FileChooser": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["frameattached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["framedetached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["framenavigated"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["load"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["pageerror"], + predicate: typing.Optional[typing.Callable[["Error"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Error": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["popup"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Response": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["websocket"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebSocket": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["worker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Worker": ... + + @typing.overload + async def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + async def wait_for_event( self, event: str, @@ -12074,28 +12379,208 @@ async def pdf( ) ) + @typing.overload def expect_event( self, - event: str, - predicate: typing.Optional[typing.Callable] = None, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, *, timeout: typing.Optional[float] = None, - ) -> AsyncEventContextManager: - """Page.expect_event + ) -> AsyncEventContextManager["Page"]: ... - Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy - value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["ConsoleMessage"]: ... - **Usage** + @typing.overload + def expect_event( + self, + event: typing.Literal["crash"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... - ```py - async with page.expect_event(\"framenavigated\") as event_info: - await page.get_by_role(\"button\") - frame = await event_info.value - ``` + @typing.overload + def expect_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Dialog"]: ... - Parameters - ---------- + @typing.overload + def expect_event( + self, + event: typing.Literal["domcontentloaded"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["download"], + predicate: typing.Optional[typing.Callable[["Download"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Download"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["filechooser"], + predicate: typing.Optional[typing.Callable[["FileChooser"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["FileChooser"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["frameattached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framedetached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framenavigated"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["load"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["pageerror"], + predicate: typing.Optional[typing.Callable[["Error"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Error"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["popup"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Response"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["websocket"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["WebSocket"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["worker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager[typing.Any]: ... + + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager: + """Page.expect_event + + Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy + value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + + **Usage** + + ```py + async with page.expect_event(\"framenavigated\") as event_info: + await page.get_by_role(\"button\") + frame = await event_info.value + ``` + + Parameters + ---------- event : str Event name, same one typically passed into `*.on(event)`. predicate : Union[Callable, None] @@ -13964,6 +14449,114 @@ async def route_from_har( ) ) + @typing.overload + def expect_event( + self, + event: typing.Literal["backgroundpage"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["BrowserContext"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["BrowserContext"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["ConsoleMessage"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Dialog"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["page"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["weberror"], + predicate: typing.Optional[typing.Callable[["WebError"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["WebError"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Response"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["serviceworker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -14089,6 +14682,114 @@ async def set_storage_state( await self._impl_obj.set_storage_state(storageState=storage_state) ) + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["backgroundpage"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["BrowserContext"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "BrowserContext": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "ConsoleMessage": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Dialog": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["page"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["weberror"], + predicate: typing.Optional[typing.Callable[["WebError"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebError": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Response": ... + + @typing.overload + async def wait_for_event( + self, + event: typing.Literal["serviceworker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Worker": ... + + @typing.overload + async def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + async def wait_for_event( self, event: str, diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index f7a8a2c24..13139abec 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -1112,6 +1112,55 @@ def url(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.url) + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["WebSocket"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framereceived"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["typing.Union[bytes, str]"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framesent"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["typing.Union[bytes, str]"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["socketerror"], + predicate: typing.Optional[typing.Callable[["str"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["str"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -1145,6 +1194,55 @@ def expect_event( ).future, ) + @typing.overload + def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebSocket": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["framereceived"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "typing.Union[bytes, str]": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["framesent"], + predicate: typing.Optional[ + typing.Callable[["typing.Union[bytes, str]"], bool] + ] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "typing.Union[bytes, str]": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["socketerror"], + predicate: typing.Optional[typing.Callable[["str"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "str": ... + + @typing.overload + def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + def wait_for_event( self, event: str, @@ -6825,6 +6923,33 @@ def evaluate_handle( ) ) + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["ConsoleMessage"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -9532,6 +9657,186 @@ def wait_for_url( ) ) + @typing.overload + def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "ConsoleMessage": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["crash"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Dialog": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["domcontentloaded"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["download"], + predicate: typing.Optional[typing.Callable[["Download"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Download": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["filechooser"], + predicate: typing.Optional[typing.Callable[["FileChooser"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "FileChooser": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["frameattached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["framedetached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["framenavigated"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Frame": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["load"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["pageerror"], + predicate: typing.Optional[typing.Callable[["Error"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Error": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["popup"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Response": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["websocket"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebSocket": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["worker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Worker": ... + + @typing.overload + def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + def wait_for_event( self, event: str, @@ -12130,28 +12435,208 @@ def pdf( ) ) + @typing.overload def expect_event( self, - event: str, - predicate: typing.Optional[typing.Callable] = None, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, *, timeout: typing.Optional[float] = None, - ) -> EventContextManager: - """Page.expect_event + ) -> EventContextManager["Page"]: ... - Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy - value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["ConsoleMessage"]: ... - **Usage** + @typing.overload + def expect_event( + self, + event: typing.Literal["crash"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... - ```py - with page.expect_event(\"framenavigated\") as event_info: - page.get_by_role(\"button\") - frame = event_info.value - ``` + @typing.overload + def expect_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Dialog"]: ... - Parameters - ---------- + @typing.overload + def expect_event( + self, + event: typing.Literal["domcontentloaded"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["download"], + predicate: typing.Optional[typing.Callable[["Download"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Download"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["filechooser"], + predicate: typing.Optional[typing.Callable[["FileChooser"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["FileChooser"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["frameattached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framedetached"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["framenavigated"], + predicate: typing.Optional[typing.Callable[["Frame"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Frame"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["load"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["pageerror"], + predicate: typing.Optional[typing.Callable[["Error"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Error"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["popup"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Response"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["websocket"], + predicate: typing.Optional[typing.Callable[["WebSocket"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["WebSocket"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["worker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager[typing.Any]: ... + + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager: + """Page.expect_event + + Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy + value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + + **Usage** + + ```py + with page.expect_event(\"framenavigated\") as event_info: + page.get_by_role(\"button\") + frame = event_info.value + ``` + + Parameters + ---------- event : str Event name, same one typically passed into `*.on(event)`. predicate : Union[Callable, None] @@ -13967,6 +14452,114 @@ def route_from_har( ) ) + @typing.overload + def expect_event( + self, + event: typing.Literal["backgroundpage"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["BrowserContext"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["BrowserContext"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["ConsoleMessage"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Dialog"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["page"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Page"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["weberror"], + predicate: typing.Optional[typing.Callable[["WebError"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["WebError"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Request"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Response"]: ... + + @typing.overload + def expect_event( + self, + event: typing.Literal["serviceworker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager["Worker"]: ... + + @typing.overload + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager[typing.Any]: ... + def expect_event( self, event: str, @@ -14092,6 +14685,114 @@ def set_storage_state( self._sync(self._impl_obj.set_storage_state(storageState=storage_state)) ) + @typing.overload + def wait_for_event( + self, + event: typing.Literal["backgroundpage"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["close"], + predicate: typing.Optional[typing.Callable[["BrowserContext"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "BrowserContext": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["console"], + predicate: typing.Optional[typing.Callable[["ConsoleMessage"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "ConsoleMessage": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["dialog"], + predicate: typing.Optional[typing.Callable[["Dialog"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Dialog": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["page"], + predicate: typing.Optional[typing.Callable[["Page"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Page": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["weberror"], + predicate: typing.Optional[typing.Callable[["WebError"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "WebError": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["request"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["requestfailed"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["requestfinished"], + predicate: typing.Optional[typing.Callable[["Request"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Request": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["response"], + predicate: typing.Optional[typing.Callable[["Response"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Response": ... + + @typing.overload + def wait_for_event( + self, + event: typing.Literal["serviceworker"], + predicate: typing.Optional[typing.Callable[["Worker"], bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> "Worker": ... + + @typing.overload + def wait_for_event( + self, + event: str, + predicate: typing.Optional[typing.Callable[..., bool]] = None, + *, + timeout: typing.Optional[float] = None, + ) -> typing.Any: ... + def wait_for_event( self, event: str, diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 4c2d9b95d..e3f1f1ebd 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -243,6 +243,55 @@ def print_events(self, class_name: str) -> None: doc.append(f" return super().{event_type}(event=event,f=f)") print("\n".join(doc)) + def print_event_overloads(self, class_name: str, method_name: str) -> None: + """Emit ``@typing.overload`` stubs for ``expect_event`` / ``wait_for_event`` + keyed on ``Literal`` event names with their payload types from api.json, + so pyright/mypy can narrow the return type at call sites. + Must be called right before the implementation signature is emitted. + """ + if class_name not in self.classes: + return + events = self.classes[class_name].get("events") or [] + if not events: + return + is_expect = method_name == "expect_event" + async_prefix = "async " if not is_expect and self.is_async else "" + if is_expect: + ctx_mgr = ( + "AsyncEventContextManager" if self.is_async else "EventContextManager" + ) + for event in events: + payload = self.serialize_doc_type(event["type"], "") + if payload.startswith("{"): + payload = "typing.Dict" + if "Union[" in payload: + payload = payload.replace("Union[", "typing.Union[") + return_type = f'{ctx_mgr}["{payload}"]' if is_expect else f'"{payload}"' + event_literal = event["name"].lower() + print(" @typing.overload") + print(f" {async_prefix}def {method_name}(") + print(" self,") + print(f' event: typing.Literal["{event_literal}"],') + print( + f' predicate: typing.Optional[typing.Callable[["{payload}"], bool]] = None,' + ) + print(" *,") + print(" timeout: typing.Optional[float] = None,") + print(f" ) -> {return_type}: ...") + print("") + # Catch-all overload for non-literal event names — keeps pyright happy + # with `event: str` callers without falling through to `Unknown`. + catchall_return = f"{ctx_mgr}[typing.Any]" if is_expect else "typing.Any" + print(" @typing.overload") + print(f" {async_prefix}def {method_name}(") + print(" self,") + print(" event: str,") + print(" predicate: typing.Optional[typing.Callable[..., bool]] = None,") + print(" *,") + print(" timeout: typing.Optional[float] = None,") + print(f" ) -> {catchall_return}: ...") + print("") + def indent_paragraph(self, p: str, indent: str) -> str: lines = p.split("\n") result = [lines[0]] diff --git a/scripts/generate_async_api.py b/scripts/generate_async_api.py index 4c0b4b655..e844bc400 100755 --- a/scripts/generate_async_api.py +++ b/scripts/generate_async_api.py @@ -92,6 +92,8 @@ def generate(t: Any) -> None: '"Disposable"', '"AsyncContextManager"' ).replace('"DisposableStub"', '"AsyncContextManager"') print("") + if name in ("expect_event", "wait_for_event"): + documentation_provider.print_event_overloads(class_name, name) async_prefix = "async " if is_async else "" print( f" {async_prefix}def {name}({signature(value, len(name) + 9)}) -> {return_type_value}:" diff --git a/scripts/generate_sync_api.py b/scripts/generate_sync_api.py index dbabce413..fa6ac8702 100755 --- a/scripts/generate_sync_api.py +++ b/scripts/generate_sync_api.py @@ -93,6 +93,8 @@ def generate(t: Any) -> None: '"Disposable"', '"SyncContextManager"' ).replace('"DisposableStub"', '"SyncContextManager"') print("") + if name in ("expect_event", "wait_for_event"): + documentation_provider.print_event_overloads(class_name, name) print( f" def {name}({signature(value, len(name) + 9)}) -> {return_type_value}:" ) diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py index 8ae14def6..cfe585c77 100644 --- a/tests/async/test_browsercontext_events.py +++ b/tests/async/test_browsercontext_events.py @@ -197,7 +197,7 @@ async def test_page_error_event_should_work(page: Page) -> None: await page.set_content('') page_error = await page_error_info.value assert page_error.page == page - assert "boom" in page_error.error.stack + assert page_error.error.stack and "boom" in page_error.error.stack async def test_weberror_event_should_work(context: BrowserContext, page: Page) -> None: diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py index a9859e87b..d294031d5 100644 --- a/tests/async/test_page_request_intercept.py +++ b/tests/async/test_page_request_intercept.py @@ -13,7 +13,6 @@ # limitations under the License. import asyncio -from typing import cast import pytest @@ -98,4 +97,4 @@ async def route_handler(route: Route) -> None: [popup, _] = await asyncio.gather( page.wait_for_event("popup"), page.get_by_text("click me").click() ) - await expect(cast(Page, popup).locator("body")).to_have_text("hello") + await expect(popup.locator("body")).to_have_text("hello") diff --git a/tests/async/test_resource_timing.py b/tests/async/test_resource_timing.py index a8481b8c8..e764e3159 100644 --- a/tests/async/test_resource_timing.py +++ b/tests/async/test_resource_timing.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict import pytest -from playwright.async_api import Browser, Page +from playwright.async_api import Browser, Page, ResourceTiming from tests.server import Server @@ -95,7 +94,7 @@ def verify_timing_value(value: float, previous: float) -> None: assert value == -1 or value > 0 and value >= previous -def verify_connections_timing_consistency(timing: Dict) -> None: +def verify_connections_timing_consistency(timing: ResourceTiming) -> None: verify_timing_value(timing["domainLookupStart"], -1) verify_timing_value(timing["domainLookupEnd"], timing["domainLookupStart"]) verify_timing_value(timing["connectStart"], timing["domainLookupEnd"]) diff --git a/tests/sync/test_resource_timing.py b/tests/sync/test_resource_timing.py index a5bd8dd8a..1c6efab6a 100644 --- a/tests/sync/test_resource_timing.py +++ b/tests/sync/test_resource_timing.py @@ -12,11 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict - import pytest -from playwright.sync_api import Browser, Page +from playwright.sync_api import Browser, Page, ResourceTiming from tests.server import Server @@ -99,7 +97,7 @@ def verify_timing_value(value: float, previous: float) -> None: assert value == -1 or value > 0 and value >= previous -def verify_connections_timing_consistency(timing: Dict) -> None: +def verify_connections_timing_consistency(timing: ResourceTiming) -> None: verify_timing_value(timing["domainLookupStart"], -1) verify_timing_value(timing["domainLookupEnd"], timing["domainLookupStart"]) verify_timing_value(timing["connectStart"], timing["domainLookupEnd"]) From 9035a6306a74c5ef05594bd445f33313143d4aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 11:19:34 +0200 Subject: [PATCH 05/22] build(deps): bump the actions group across 1 directory with 4 updates (#3049) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/publish.yml | 6 +++--- .github/workflows/publish_docker.yml | 6 +++--- .github/workflows/test_docker.yml | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb06caff0..fb1bf4961 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -91,7 +91,7 @@ jobs: browser: chromium runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -138,7 +138,7 @@ jobs: browser-channel: msedge runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -181,11 +181,11 @@ jobs: # where the default shell is pwsh and skips the activation hooks. shell: bash -el {0} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get conda - uses: conda-incubator/setup-miniconda@v3 + uses: conda-incubator/setup-miniconda@v4 with: python-version: '3.12' channels: conda-forge @@ -202,7 +202,7 @@ jobs: run: working-directory: examples/todomvc/ steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e9a7048c5..48087c53f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,14 +22,14 @@ jobs: runs-on: ${{ matrix.os }} defaults: run: - # Required for conda-incubator/setup-miniconda@v3 + # Required for conda-incubator/setup-miniconda@v4 shell: bash -el {0} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get conda - uses: conda-incubator/setup-miniconda@v3 + uses: conda-incubator/setup-miniconda@v4 with: python-version: '3.12' channels: conda-forge diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 7494f1abc..83fa5b9b9 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -15,9 +15,9 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: Docker steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Azure login - uses: azure/login@v2 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }} @@ -29,7 +29,7 @@ jobs: with: python-version: "3.10" - name: Set up Docker QEMU for arm64 docker builds - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: platforms: arm64 - name: Install dependencies & browsers diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index 464eb3b46..a25a382b5 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -30,7 +30,7 @@ jobs: - ubuntu-24.04 - ubuntu-24.04-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: From 58e36b3f40da9c0c783379931a5f125cb3f54a10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 11:19:41 +0200 Subject: [PATCH 06/22] build(deps): bump pyopenssl from 25.1.0 to 26.0.0 in the pip group across 1 directory (#3041) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 5735ae2a0..49c59db57 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -8,7 +8,7 @@ objgraph==3.6.2 Pillow==11.3.0 pixelmatch==0.3.0 pre-commit==3.5.0 -pyOpenSSL==25.1.0 +pyOpenSSL==26.0.0 pytest==8.4.1 pytest-asyncio==1.1.0 pytest-cov==6.3.0 From 2a384b123213592e4955be6a275188d1e902f1f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 11:19:47 +0200 Subject: [PATCH 07/22] build(deps): bump setuptools-scm from 8.3.1 to 9.2.2 (#2994) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ff674eab..98ef2e892 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.9.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.9.0", "setuptools-scm==9.2.2", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] From deb2df8834cd0fd4b4031bf17a63c6933e42c740 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 12:15:36 +0200 Subject: [PATCH 08/22] build(deps): bump pytest-cov from 6.3.0 to 7.1.0 (#2976) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 49c59db57..38d503a9d 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -11,7 +11,7 @@ pre-commit==3.5.0 pyOpenSSL==26.0.0 pytest==8.4.1 pytest-asyncio==1.1.0 -pytest-cov==6.3.0 +pytest-cov==7.1.0 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 From ac9bf29e5239817ce9eae903f576662fb83c3941 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 12:16:00 +0200 Subject: [PATCH 09/22] build(deps): bump types-requests from 2.32.4.20250809 to 2.32.4.20260107 (#2975) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 38d503a9d..bf25f6390 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -20,4 +20,4 @@ requests==2.32.5 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.4.20250809 +types-requests==2.32.4.20260107 From 161bd3dfb3a57a34479262101a7f10709c86e0ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 12:16:08 +0200 Subject: [PATCH 10/22] build(deps): bump flake8 from 7.2.0 to 7.3.0 (#2896) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index bf25f6390..43533b453 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -2,7 +2,7 @@ asyncio-atexit==1.0.1 autobahn==23.1.2 black==25.1.0 build==1.3.0 -flake8==7.2.0 +flake8==7.3.0 mypy==1.17.1 objgraph==3.6.2 Pillow==11.3.0 From 856ae37b0b8262684a43cd34a2a74207e9ca5739 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 5 May 2026 10:08:14 +0200 Subject: [PATCH 11/22] devops: Update EsrpRelease (#3064) --- .azure-pipelines/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index 1a2e0e7a4..0b8c37402 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -60,7 +60,7 @@ extends: targetPath: $(Build.ArtifactStagingDirectory)/esrp-build steps: - checkout: none - - task: EsrpRelease@9 + - task: EsrpRelease@11 inputs: connectedservicename: 'Playwright-ESRP-PME' usemanagedidentity: true From 9df9fac6137bfc4948f5c6774bdbb8669886bb4a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 5 May 2026 10:09:20 +0200 Subject: [PATCH 12/22] feat: accept datetime.timedelta for timeout parameters (#3059) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playwright/_impl/_helper.py | 11 + playwright/async_api/_generated.py | 1191 +++++++++++++++++---------- playwright/sync_api/_generated.py | 1231 ++++++++++++++++++---------- scripts/generate_api.py | 9 + tests/async/test_click.py | 6 + tests/sync/test_sync.py | 6 + 6 files changed, 1578 insertions(+), 876 deletions(-) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index dc0a2479d..213fdc1e3 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio +import datetime import math import os import re @@ -574,3 +575,13 @@ def is_file_payload(value: Optional[Any]) -> bool: def is_textual_mime_type(mime_type: str) -> bool: return bool(TEXTUAL_MIME_TYPE.match(mime_type)) + + +def to_milliseconds( + value: Union[float, datetime.timedelta, None], +) -> Optional[float]: + if value is None: + return None + if isinstance(value, datetime.timedelta): + return value / datetime.timedelta(milliseconds=1) + return value diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 9e8737c12..5270ed29e 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -71,6 +71,7 @@ from playwright._impl._fetch import APIResponse as APIResponseImpl from playwright._impl._file_chooser import FileChooser as FileChooserImpl from playwright._impl._frame import Frame as FrameImpl +from playwright._impl._helper import to_milliseconds from playwright._impl._input import Keyboard as KeyboardImpl from playwright._impl._input import Mouse as MouseImpl from playwright._impl._input import Touchscreen as TouchscreenImpl @@ -791,7 +792,7 @@ async def fetch( post_data: typing.Optional[typing.Union[typing.Any, str, bytes]] = None, max_redirects: typing.Optional[int] = None, max_retries: typing.Optional[int] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, ) -> "APIResponse": """Route.fetch @@ -850,7 +851,7 @@ async def handle(route): postData=mapping.to_impl(post_data), maxRedirects=max_redirects, maxRetries=max_retries, - timeout=timeout, + timeout=to_milliseconds(timeout), ) ) @@ -1174,7 +1175,7 @@ def expect_event( event: str, predicate: typing.Optional[typing.Callable] = None, *, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, ) -> AsyncEventContextManager: """WebSocket.expect_event @@ -1198,7 +1199,9 @@ def expect_event( return AsyncEventContextManager( self._impl_obj.expect_event( - event=event, predicate=self._wrap_handler(predicate), timeout=timeout + event=event, + predicate=self._wrap_handler(predicate), + timeout=to_milliseconds(timeout), ).future ) @@ -1256,7 +1259,7 @@ async def wait_for_event( event: str, predicate: typing.Optional[typing.Callable] = None, *, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, ) -> typing.Any: """WebSocket.wait_for_event @@ -1283,7 +1286,9 @@ async def wait_for_event( return mapping.from_maybe_impl( await self._impl_obj.wait_for_event( - event=event, predicate=self._wrap_handler(predicate), timeout=timeout + event=event, + predicate=self._wrap_handler(predicate), + timeout=to_milliseconds(timeout), ) ) @@ -2142,7 +2147,9 @@ async def dispatch_event( ) async def scroll_into_view_if_needed( - self, *, timeout: typing.Optional[float] = None + self, + *, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, ) -> None: """ElementHandle.scroll_into_view_if_needed @@ -2163,7 +2170,9 @@ async def scroll_into_view_if_needed( """ return mapping.from_maybe_impl( - await self._impl_obj.scroll_into_view_if_needed(timeout=timeout) + await self._impl_obj.scroll_into_view_if_needed( + timeout=to_milliseconds(timeout) + ) ) async def hover( @@ -2173,7 +2182,7 @@ async def hover( typing.Sequence[Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"]] ] = None, position: typing.Optional[Position] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, no_wait_after: typing.Optional[bool] = None, force: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, @@ -2216,7 +2225,7 @@ async def hover( await self._impl_obj.hover( modifiers=mapping.to_impl(modifiers), position=position, - timeout=timeout, + timeout=to_milliseconds(timeout), noWaitAfter=no_wait_after, force=force, trial=trial, @@ -2233,7 +2242,7 @@ async def click( delay: typing.Optional[float] = None, button: typing.Optional[Literal["left", "middle", "right"]] = None, click_count: typing.Optional[int] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, @@ -2292,7 +2301,7 @@ async def click( delay=delay, button=button, clickCount=click_count, - timeout=timeout, + timeout=to_milliseconds(timeout), force=force, noWaitAfter=no_wait_after, trial=trial, @@ -2309,7 +2318,7 @@ async def dblclick( position: typing.Optional[Position] = None, delay: typing.Optional[float] = None, button: typing.Optional[Literal["left", "middle", "right"]] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, @@ -2364,7 +2373,7 @@ async def dblclick( position=position, delay=delay, button=button, - timeout=timeout, + timeout=to_milliseconds(timeout), force=force, noWaitAfter=no_wait_after, trial=trial, @@ -2381,7 +2390,7 @@ async def select_option( element: typing.Optional[ typing.Union["ElementHandle", typing.Sequence["ElementHandle"]] ] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, ) -> typing.List[str]: @@ -2442,7 +2451,7 @@ async def select_option( index=mapping.to_impl(index), label=mapping.to_impl(label), element=mapping.to_impl(element), - timeout=timeout, + timeout=to_milliseconds(timeout), force=force, noWaitAfter=no_wait_after, ) @@ -2455,7 +2464,7 @@ async def tap( typing.Sequence[Literal["Alt", "Control", "ControlOrMeta", "Meta", "Shift"]] ] = None, position: typing.Optional[Position] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, @@ -2500,7 +2509,7 @@ async def tap( await self._impl_obj.tap( modifiers=mapping.to_impl(modifiers), position=position, - timeout=timeout, + timeout=to_milliseconds(timeout), force=force, noWaitAfter=no_wait_after, trial=trial, @@ -2511,7 +2520,7 @@ async def fill( self, value: str, *, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, no_wait_after: typing.Optional[bool] = None, force: typing.Optional[bool] = None, ) -> None: @@ -2543,7 +2552,10 @@ async def fill( return mapping.from_maybe_impl( await self._impl_obj.fill( - value=value, timeout=timeout, noWaitAfter=no_wait_after, force=force + value=value, + timeout=to_milliseconds(timeout), + noWaitAfter=no_wait_after, + force=force, ) ) @@ -2551,7 +2563,7 @@ async def select_text( self, *, force: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, ) -> None: """ElementHandle.select_text @@ -2572,10 +2584,16 @@ async def select_text( """ return mapping.from_maybe_impl( - await self._impl_obj.select_text(force=force, timeout=timeout) + await self._impl_obj.select_text( + force=force, timeout=to_milliseconds(timeout) + ) ) - async def input_value(self, *, timeout: typing.Optional[float] = None) -> str: + async def input_value( + self, + *, + timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, + ) -> str: """ElementHandle.input_value Returns `input.value` for the selected `` or `