Skip to content

Commit 613c3bf

Browse files
authored
chore(roll): v1.61.0 (#3102)
1 parent f7c6259 commit 613c3bf

29 files changed

Lines changed: 1186 additions & 97 deletions

.claude/skills/playwright-roll/SKILL.md

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,6 @@ The upstream documentation source of truth is `docs/src/api/*.md` in the playwri
2727

2828
> **The mistake the 1.59 roll made twice over:** classifying things as "internal tooling, N/A for Python" based on the *name* of the API (Screencast, Debugger, pickLocator, clearConsoleMessages, artifactsDir, …). Almost all of those had empty `langs: {}` in `api.json` and were real Python APIs. Sounding tooling-y is not a `langs` filter. **The `langs` field on the member in `api.json` is the only authoritative signal.** When in doubt, dump it (see "Verifying classifications" below).
2929
30-
## Pre-flight
31-
32-
You will need two checkouts in the parent directory:
33-
- `~/code/playwright-python` — this repo.
34-
- `~/code/playwright` — the upstream playwright monorepo (used read-only for diffing).
35-
36-
Bring upstream up to date and ensure release branches/tags are present:
37-
38-
```sh
39-
git -C ~/code/playwright fetch --tags
40-
git -C ~/code/playwright fetch origin 'release-*:release-*'
41-
```
42-
43-
There is sometimes no `vX.Y.0` tag for the latest release (the bots cut release branches first and tag later). Anchor on commits, not tags — see "Identify the commit range" below.
44-
4530
## Process
4631

4732
### 1. Set up the env
@@ -76,18 +61,29 @@ build + per-platform Node downloads).
7661

7762
### 3. Identify the commit range
7863

64+
The build step (step 2) clones the upstream monorepo into `driver/playwright-src`.
65+
Bring it up to date and ensure release branches/tags are present before walking
66+
the range:
67+
68+
```sh
69+
git -C driver/playwright-src fetch --tags
70+
git -C driver/playwright-src fetch origin 'release-*:release-*'
71+
```
72+
73+
There is sometimes no `vX.Y.0` tag for the latest release (the bots cut release branches first and tag later). Anchor on commits, not tags.
74+
7975
The diff range is "every commit on the new release branch since the previous release was cut". Anchor commits:
8076

8177
- **Previous release end**: the `chore: bump version to vX.Y.0-next` commit on `main`. That commit is the first commit *after* the previous release (X.Y-1) was cut. Use its parent (`<sha>~1`) as the lower bound.
8278
```sh
83-
git -C ~/code/playwright log --all --grep="bump version to v" --oneline | head
79+
git -C driver/playwright-src log --all --grep="bump version to v" --oneline | head
8480
```
8581
- **New release end**: the tip of `release-<new>` (or the matching tag if it exists).
8682

8783
Save the commit list, oldest first, scoped to `docs/src/api/`:
8884

8985
```sh
90-
git -C ~/code/playwright log <prev-anchor>~1..release-<new> --oneline --reverse -- docs/src/api > /tmp/roll-<new>-commits.md
86+
git -C driver/playwright-src log <prev-anchor>~1..release-<new> --oneline --reverse -- docs/src/api > /tmp/roll-<new>-commits.md
9187
```
9288

9389
A normal roll yields 50–100 commits. If you see 0 or thousands, the range is wrong.
@@ -99,7 +95,7 @@ Format the file as a markdown checklist and add the standard preamble (status le
9995
For each commit, in chronological order:
10096

10197
```sh
102-
git -C ~/code/playwright show <sha> -- docs/src/api/
98+
git -C driver/playwright-src show <sha> -- docs/src/api/
10399
```
104100

105101
Look for:
@@ -144,7 +140,7 @@ A few rules of thumb that catch most "actually a PORT" cases:
144140

145141
#### PORT
146142

147-
Implement the change in `playwright/_impl/<module>.py`. Use the upstream JS implementation as a reference: `~/code/playwright/packages/playwright-core/src/client/<module>.ts`. Translate idioms:
143+
Implement the change in `playwright/_impl/<module>.py`. Use the upstream JS implementation as a reference: `driver/playwright-src/packages/playwright-core/src/client/<module>.ts`. Translate idioms:
148144

149145
| Upstream JS | Python |
150146
|---|---|
@@ -285,7 +281,7 @@ Class names use the upstream PascalCase (`BrowserContext`, `BrowserType`); metho
285281
- **A cluster of suppressions on the same class is a smell.** If you're about to add five `Method not implemented: Foo.*` lines, you're almost certainly looking at a class that needs to be implemented. Implement the whole thing once and the suppressions disappear.
286282
- **Watch for revert pairs in the same range.** 1.59 added and reverted `Browser.isRemote` (#39613 / #39620) inside the same release. Walking chronologically lets you skip the add when you see the revert later.
287283
- **Watch for rename-revert pairs.** 1.59 had `Locator.normalize``Locator.toCode` (#39648) → `Locator.normalize` (#39754). Final state wins; only port the last.
288-
- **Doc renames almost always include a wire-protocol rename.** Whenever you see `### param: X.y.oldName``### param: X.y.newName` in a doc commit, also `git -C ~/code/playwright show <sha> -- packages/protocol/src/protocol.yml` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the channel-send dict key in `_impl/` must change. Suppressing the doc-side mismatch is hiding a real bug — the previous Python code is silently sending an unknown field that the new server ignores.
284+
- **Doc renames almost always include a wire-protocol rename.** Whenever you see `### param: X.y.oldName``### param: X.y.newName` in a doc commit, also `git -C driver/playwright-src show <sha> -- packages/protocol/src/protocol.yml` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the channel-send dict key in `_impl/` must change. Suppressing the doc-side mismatch is hiding a real bug — the previous Python code is silently sending an unknown field that the new server ignores.
289285
- **TypedDicts beat `Dict[str, X]` for any structured return.** When the docs describe a return as `[Object]` with named fields (or even `[Object=Foo]`), define a `TypedDict` in `_api_structures.py`, re-export from both public `__init__.py` files, and use it. Zero runtime cost (it's still a `dict`), and the doc generator's type comparator matches by structure via `get_type_hints`.
290286
- **Positional renames are free.** A param with no default before any `*` separator is positional-or-keyword in Python, but realistic call sites pass it positionally. Renaming such a param doesn't break callers.
291287
- **The "Backport changes" GitHub issue can be misleading.** In the 1.59 roll its checkboxes were all marked `[x]` with annotations like "✅ IMPLEMENTED", but several of those features had not actually been merged into the Python port. Trust the `docs/src/api/` walk over the issue.

DRIVER_SHA

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
87bb9ddbd78f329df18c2b24847bc9409240cd07
1+
1cc5a90cfa3eaa430b1a991963100f95126caa47

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
44

55
| | Linux | macOS | Windows |
66
| :--- | :---: | :---: | :---: |
7-
| Chromium <!-- GEN:chromium-version -->148.0.7778.96<!-- GEN:stop --> ||||
8-
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> ||||
9-
| Firefox <!-- GEN:firefox-version -->150.0.2<!-- GEN:stop --> ||||
7+
| Chromium <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> ||||
8+
| WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> ||||
9+
| Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> ||||
1010

1111
## Documentation
1212

playwright/_impl/_api_structures.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,12 @@ class FrameExpectOptions(TypedDict, total=False):
234234
pseudo: Optional[str]
235235

236236

237-
class FrameExpectResult(TypedDict):
237+
class FrameExpectResult(TypedDict, total=False):
238238
matches: bool
239239
received: Any
240-
log: List[str]
240+
log: Optional[List[str]]
241241
errorMessage: Optional[str]
242+
timedOut: Optional[bool]
242243

243244

244245
AriaRole = Literal[
@@ -344,7 +345,21 @@ class DebuggerPausedDetails(TypedDict):
344345
title: str
345346

346347

348+
class ScreencastSize(TypedDict):
349+
width: int
350+
height: int
351+
352+
353+
class VirtualCredential(TypedDict):
354+
id: str
355+
rpId: str
356+
userHandle: str
357+
privateKey: str
358+
publicKey: str
359+
360+
347361
class ScreencastFrame(TypedDict):
348362
data: bytes
363+
timestamp: float
349364
viewportWidth: int
350365
viewportHeight: int

playwright/_impl/_browser_context.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from_nullable_channel,
4747
)
4848
from playwright._impl._console_message import ConsoleMessage
49+
from playwright._impl._credentials import Credentials
4950
from playwright._impl._debugger import Debugger
5051
from playwright._impl._dialog import Dialog
5152
from playwright._impl._disposable import Disposable, DisposableStub
@@ -133,6 +134,7 @@ def __init__(
133134
self._request: APIRequestContext = from_channel(initializer["requestContext"])
134135
self._request._timeout_settings = self._timeout_settings
135136
self._clock = Clock(self)
137+
self._credentials = Credentials(self)
136138
self._channel.on(
137139
"bindingCall",
138140
lambda params: self._on_binding(from_channel(params["binding"])),
@@ -741,3 +743,7 @@ def request(self) -> "APIRequestContext":
741743
@property
742744
def clock(self) -> Clock:
743745
return self._clock
746+
747+
@property
748+
def credentials(self) -> Credentials:
749+
return self._credentials

playwright/_impl/_browser_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ async def connect_over_cdp(
197197
headers: Dict[str, str] = None,
198198
isLocal: bool = None,
199199
noDefaults: bool = None,
200+
artifactsDir: Union[str, Path] = None,
200201
) -> Browser:
201202
params = locals_to_params(locals())
202203
if params.get("headers"):

playwright/_impl/_connection.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def send_no_reply(
9494
is_internal: bool = False,
9595
title: str = None,
9696
) -> None:
97-
# No reply messages are used to e.g. waitForEventInfo(after).
97+
# No reply messages are used to e.g. __waitInfo__(after).
9898
self._connection.wrap_api_call_sync(
9999
lambda: self._connection._send_message_to_server(
100100
self._object,
@@ -413,7 +413,7 @@ def dispatch(self, msg: ParsedMessagePayload) -> None:
413413
callback = self._callbacks.pop(id)
414414
if callback.future.cancelled():
415415
return
416-
# No reply messages are used to e.g. waitForEventInfo(after) which returns exceptions on page close.
416+
# No reply messages are used to e.g. __waitInfo__(after) which returns exceptions on page close.
417417
# To prevent 'Future exception was never retrieved' we just ignore such messages.
418418
if callback.no_reply:
419419
return
@@ -422,6 +422,10 @@ def dispatch(self, msg: ParsedMessagePayload) -> None:
422422
parsed_error = parse_error(
423423
error["error"], format_call_log(msg.get("log")) # type: ignore
424424
)
425+
parsed_error._log = msg.get("log")
426+
parsed_error._details = self._replace_guids_with_channels(
427+
msg.get("errorDetails")
428+
)
425429
parsed_error._stack = "".join(callback.stack_trace.format())
426430
callback.future.set_exception(parsed_error)
427431
else:

playwright/_impl/_credentials.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import TYPE_CHECKING, List
16+
17+
from playwright._impl._api_structures import VirtualCredential
18+
from playwright._impl._helper import locals_to_params
19+
20+
if TYPE_CHECKING:
21+
from playwright._impl._browser_context import BrowserContext
22+
23+
24+
class Credentials:
25+
def __init__(self, browser_context: "BrowserContext") -> None:
26+
self._browser_context = browser_context
27+
self._loop = browser_context._loop
28+
self._dispatcher_fiber = browser_context._dispatcher_fiber
29+
30+
async def install(self) -> None:
31+
await self._browser_context._channel.send("credentialsInstall", None)
32+
33+
async def create(
34+
self,
35+
rpId: str,
36+
id: str = None,
37+
userHandle: str = None,
38+
privateKey: str = None,
39+
publicKey: str = None,
40+
) -> VirtualCredential:
41+
result = await self._browser_context._channel.send_return_as_dict(
42+
"credentialsCreate", None, locals_to_params(locals())
43+
)
44+
return (result or {})["credential"]
45+
46+
async def delete(self, id: str) -> None:
47+
await self._browser_context._channel.send("credentialsDelete", None, {"id": id})
48+
49+
async def get(
50+
self,
51+
rpId: str = None,
52+
id: str = None,
53+
) -> List[VirtualCredential]:
54+
result = await self._browser_context._channel.send_return_as_dict(
55+
"credentialsGet", None, locals_to_params(locals())
56+
)
57+
return (result or {}).get("credentials", [])

playwright/_impl/_errors.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# stable API.
1717

1818

19-
from typing import Optional
19+
from typing import Any, List, Optional
2020

2121

2222
def is_target_closed_error(error: Exception) -> bool:
@@ -28,6 +28,8 @@ def __init__(self, message: str) -> None:
2828
self._message = message
2929
self._name: Optional[str] = None
3030
self._stack: Optional[str] = None
31+
self._details: Optional[Any] = None
32+
self._log: Optional[List[str]] = None
3133
super().__init__(message)
3234

3335
@property
@@ -57,4 +59,6 @@ def rewrite_error(error: Exception, message: str) -> Exception:
5759
if isinstance(rewritten_exc, Error) and isinstance(error, Error):
5860
rewritten_exc._name = error.name
5961
rewritten_exc._stack = error.stack
62+
rewritten_exc._details = error._details
63+
rewritten_exc._log = error._log
6064
return rewritten_exc

playwright/_impl/_fetch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
Headers,
2929
HttpCredentials,
3030
ProxySettings,
31+
RemoteAddr,
32+
SecurityDetails,
3133
ServerFilePayload,
3234
StorageState,
3335
)
@@ -557,6 +559,12 @@ async def json(self) -> Any:
557559
content = await self.text()
558560
return json.loads(content)
559561

562+
async def security_details(self) -> Optional[SecurityDetails]:
563+
return self._initializer.get("securityDetails") or None
564+
565+
async def server_addr(self) -> Optional[RemoteAddr]:
566+
return self._initializer.get("serverAddr") or None
567+
560568
async def dispose(self) -> None:
561569
await self._request._channel.send(
562570
"disposeAPIResponse",

0 commit comments

Comments
 (0)