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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DRIVER_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.61.1-beta-1782139630000
1.62.0-alpha-2026-07-16
2 changes: 1 addition & 1 deletion NODE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.17.0
24.18.0
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->151.0.7922.19<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->152.0.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
20 changes: 14 additions & 6 deletions playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
WebSocketRouteHandlerCallback,
async_readfile,
async_writefile,
create_task_and_ignore_exception,
locals_to_params,
parse_error,
to_impl,
Expand Down Expand Up @@ -260,10 +261,11 @@ async def _on_route(self, route: Route) -> None:
handled = await route_handler.handle(route)
finally:
if len(self._routes) == 0:
asyncio.create_task(
create_task_and_ignore_exception(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? Can you link to the upstream counterpart?

self._loop,
self._connection.wrap_api_call(
lambda: self._update_interception_patterns(), True
)
),
)
if handled:
return
Expand Down Expand Up @@ -497,6 +499,7 @@ async def route_from_har(
update: bool = None,
updateContent: Literal["attach", "embed"] = None,
updateMode: HarMode = None,
interceptAPIRequests: bool = None,
) -> None:
if update:
await self._tracing._record_into_har(
Expand All @@ -515,6 +518,8 @@ async def route_from_har(
)
self._har_routers.append(router)
await router.add_context_route(self)
if interceptAPIRequests:
await router.add_api_request_route(self)

async def _update_interception_patterns(self) -> None:
patterns = RouteHandler.prepare_interception_patterns(self._routes)
Expand Down Expand Up @@ -586,10 +591,13 @@ async def _inner_close() -> None:
await self._closed_future

async def storage_state(
self, path: Union[str, Path] = None, indexedDB: bool = None
self,
path: Union[str, Path] = None,
indexedDB: bool = None,
credentials: bool = None,
) -> StorageState:
result = await self._channel.send_return_as_dict(
"storageState", None, {"indexedDB": indexedDB}
"storageState", None, {"indexedDB": indexedDB, "credentials": credentials}
)
if path:
await async_writefile(path, json.dumps(result))
Expand Down Expand Up @@ -685,9 +693,9 @@ def _on_dialog(self, dialog: Dialog) -> None:
# a) removing "dialog" listener subscription (client->server)
# b) actual "dialog" event (server->client)
if dialog.type == "beforeunload":
asyncio.create_task(dialog.accept())
create_task_and_ignore_exception(self._loop, dialog.accept())
else:
asyncio.create_task(dialog.dismiss())
create_task_and_ignore_exception(self._loop, dialog.dismiss())

def _on_page_error(
self, error: Error, page: Optional[Page], location: WebErrorLocation
Expand Down
8 changes: 5 additions & 3 deletions playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,20 @@ async def connect(
) -> Browser:
if slowMo is None:
slowMo = 0
if timeout is None:
timeout = 0

headers = {**(headers if headers else {}), "x-playwright-browser": self.name}
local_utils = self._connection.local_utils
pipe_channel = (
await local_utils._channel.send_return_as_dict(
"connect",
None,
lambda t: t or 0,
{
"endpoint": endpoint,
"headers": headers,
"slowMo": slowMo,
"timeout": timeout if timeout is not None else 0,
"timeout": timeout,
"exposeNetwork": exposeNetwork,
},
)
Expand Down Expand Up @@ -272,7 +274,7 @@ def handle_transport_close(reason: Optional[str]) -> None:
playwright_future = connection.playwright_future

timeout_future = throw_on_timeout(
timeout if timeout is not None else PLAYWRIGHT_MAX_DEADLINE,
timeout if timeout else PLAYWRIGHT_MAX_DEADLINE,
Error("Connection timed out"),
)
done, pending = await asyncio.wait(
Expand Down
24 changes: 18 additions & 6 deletions playwright/_impl/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
List,
Mapping,
Optional,
Tuple,
TypedDict,
Union,
cast,
Expand Down Expand Up @@ -95,11 +96,13 @@ def send_no_reply(
title: str = None,
) -> None:
# No reply messages are used to e.g. __waitInfo__(after).
augmented_params, timeout = _augment_params(params, timeout_calculator)
self._connection.wrap_api_call_sync(
lambda: self._connection._send_message_to_server(
self._object,
method,
_augment_params(params, timeout_calculator),
augmented_params,
timeout,
True,
),
is_internal,
Expand All @@ -117,8 +120,9 @@ async def _inner_send(
error = self._connection._error
self._connection._error = None
raise error
augmented_params, timeout = _augment_params(params, timeout_calculator)
callback = self._connection._send_message_to_server(
self._object, method, _augment_params(params, timeout_calculator)
self._object, method, augmented_params, timeout
)
done, _ = await asyncio.wait(
{
Expand Down Expand Up @@ -352,7 +356,12 @@ def set_is_tracing(self, is_tracing: bool) -> None:
self._tracing_count -= 1

def _send_message_to_server(
self, object: ChannelOwner, method: str, params: Dict, no_reply: bool = False
self,
object: ChannelOwner,
method: str,
params: Dict,
timeout: float,
no_reply: bool = False,
) -> ProtocolCallback:
if self._closed_error:
raise self._closed_error
Expand Down Expand Up @@ -384,6 +393,7 @@ def _send_message_to_server(
"wallTime": int(datetime.datetime.now().timestamp() * 1000),
"apiName": stack_trace_information["apiName"],
"internal": not stack_trace_information["apiName"],
"timeout": timeout,
}
if location:
metadata["location"] = location # type: ignore
Expand Down Expand Up @@ -652,12 +662,14 @@ def _extract_stack_trace_information_from_stack(
def _augment_params(
params: Optional[Dict],
timeout_calculator: Optional[Callable[[Optional[float]], float]],
) -> Dict:
) -> Tuple[Dict, float]:
if params is None:
params = {}
timeout_param = params.pop("timeout", None)
timeout: float = 0
if timeout_calculator:
params["timeout"] = timeout_calculator(params.get("timeout"))
return _filter_none(params)
timeout = timeout_calculator(timeout_param)
return _filter_none(params), timeout


def _filter_none(d: Mapping) -> Dict:
Expand Down
28 changes: 21 additions & 7 deletions playwright/_impl/_element_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.

import base64
import mimetypes
from pathlib import Path
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -122,6 +121,7 @@ async def hover(
noWaitAfter: bool = None,
force: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"hover", self._frame._timeout, locals_to_params(locals())
Expand All @@ -139,6 +139,7 @@ async def click(
noWaitAfter: bool = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"click", self._frame._timeout, locals_to_params(locals())
Expand All @@ -155,6 +156,7 @@ async def dblclick(
noWaitAfter: bool = None,
trial: bool = None,
steps: int = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"dblclick", self._frame._timeout, locals_to_params(locals())
Expand Down Expand Up @@ -187,6 +189,7 @@ async def tap(
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"tap", self._frame._timeout, locals_to_params(locals())
Expand Down Expand Up @@ -267,20 +270,23 @@ async def set_checked(
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
if checked:
await self.check(
position=position,
timeout=timeout,
force=force,
trial=trial,
scroll=scroll,
)
else:
await self.uncheck(
position=position,
timeout=timeout,
force=force,
trial=trial,
scroll=scroll,
)

async def check(
Expand All @@ -290,6 +296,7 @@ async def check(
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"check", self._frame._timeout, locals_to_params(locals())
Expand All @@ -302,6 +309,7 @@ async def uncheck(
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
scroll: Literal["auto", "none"] = None,
) -> None:
await self._channel.send(
"uncheck", self._frame._timeout, locals_to_params(locals())
Expand All @@ -313,7 +321,7 @@ async def bounding_box(self) -> Optional[FloatRect]:
async def screenshot(
self,
timeout: float = None,
type: Literal["jpeg", "png"] = None,
type: Literal["jpeg", "png", "webp"] = None,
path: Union[str, Path] = None,
quality: int = None,
omitBackground: bool = None,
Expand Down Expand Up @@ -457,10 +465,16 @@ def convert_select_option_values(
return dict(options=options, elements=elements)


def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png"]:
mime_type, _ = mimetypes.guess_type(path)
if mime_type == "image/png":
def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png", "webp"]:
# Detect by file extension rather than mimetypes.guess_type, whose result is
# OS-dependent (e.g. Windows does not register image/webp), mirroring
# upstream's getMimeTypeForPath extension map:
# https://github.com/microsoft/playwright/blob/e0e814deed7b0a4c4d2bdf98481e6be7419cda16/packages/isomorphic/mimeType.ts#L28
suffix = Path(path).suffix.lower()
if suffix == ".png":
return "png"
if mime_type == "image/jpeg":
if suffix in (".jpeg", ".jpg"):
return "jpeg"
raise Error(f'Unsupported screenshot mime type for path "{path}": {mime_type}')
if suffix == ".webp":
return "webp"
raise Error(f'path: unsupported mime type "{suffix}"')
19 changes: 19 additions & 0 deletions playwright/_impl/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
HttpCredentials,
ProxySettings,
RemoteAddr,
ResourceTiming,
SecurityDetails,
ServerFilePayload,
StorageState,
Expand Down Expand Up @@ -531,6 +532,24 @@ def headers(self) -> Headers:
def headers_array(self) -> network.HeadersArray:
return self._headers.headers_array()

@property
def timing(self) -> ResourceTiming:
return cast(
ResourceTiming,
{
"startTime": -1,
"domainLookupStart": -1,
"domainLookupEnd": -1,
"connectStart": -1,
"secureConnectionStart": -1,
"connectEnd": -1,
"requestStart": -1,
"responseStart": -1,
**self._initializer.get("timing", {}),
"responseEnd": self._initializer.get("responseEndTiming", -1),
},
)

async def body(self) -> bytes:
try:
result = await self._request._connection.wrap_api_call(
Expand Down
Loading
Loading