Skip to content

Commit 020cb13

Browse files
committed
feat(roll): update driver to ac7cdd4bd, port new APIs, add tests
Driver SHA: ac7cdd4bdf15f90fe7229243be6b35a53e0296d1 (v1.61.0-next) New APIs: - APIResponse.security_details / .server_addr - Credentials class (WebAuthn) + BrowserContext.credentials - WebStorage class + Page.local_storage / .session_storage - Screencast.start(size=), .show_actions(cursor=), ScreencastFrame.timestamp - BrowserType.connect_over_cdp(artifacts_dir=) Also: - Stop forcing options-bag properties to required=False in documentation_provider.py (Credentials.create(rp_id=) was the only case affected) - Update rolling skill to use driver/playwright-src - 37 new tests (async + sync)
1 parent 95db482 commit 020cb13

24 files changed

Lines changed: 1122 additions & 51 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+
ac7cdd4bdf15f90fe7229243be6b35a53e0296d1

README.md

Lines changed: 2 additions & 2 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 --> ||||
7+
| Chromium <!-- GEN:chromium-version -->149.0.7827.22<!-- GEN:stop --> ||||
88
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> ||||
9-
| Firefox <!-- GEN:firefox-version -->150.0.2<!-- GEN:stop --> ||||
9+
| Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> ||||
1010

1111
## Documentation
1212

playwright/_impl/_api_structures.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,21 @@ class DebuggerPausedDetails(TypedDict):
344344
title: str
345345

346346

347+
class ScreencastSize(TypedDict):
348+
width: int
349+
height: int
350+
351+
352+
class VirtualCredential(TypedDict):
353+
id: str
354+
rpId: str
355+
userHandle: str
356+
privateKey: str
357+
publicKey: str
358+
359+
347360
class ScreencastFrame(TypedDict):
348361
data: bytes
362+
timestamp: float
349363
viewportWidth: int
350364
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
@@ -202,6 +202,7 @@ async def connect_over_cdp(
202202
headers: Dict[str, str] = None,
203203
isLocal: bool = None,
204204
noDefaults: bool = None,
205+
artifactsDir: Union[str, Path] = None,
205206
) -> Browser:
206207
params = locals_to_params(locals())
207208
if params.get("headers"):

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/_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",

playwright/_impl/_page.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
from playwright._impl._screencast import Screencast
104104
from playwright._impl._video import Video
105105
from playwright._impl._waiter import Waiter
106+
from playwright._impl._web_storage import WebStorage
106107

107108
if TYPE_CHECKING: # pragma: no cover
108109
from playwright._impl._browser_context import BrowserContext
@@ -183,6 +184,8 @@ def __init__(
183184
cast(Optional[Artifact], from_nullable_channel(initializer.get("video"))),
184185
)
185186
self._screencast: Screencast = Screencast(self)
187+
self._local_storage = WebStorage(self, "local")
188+
self._session_storage = WebStorage(self, "session")
186189
self._opener = cast("Page", from_nullable_channel(initializer.get("opener")))
187190
self._close_reason: Optional[str] = None
188191
self._close_was_called = False
@@ -1206,6 +1209,14 @@ def video(self) -> Optional[Video]:
12061209
def screencast(self) -> Screencast:
12071210
return self._screencast
12081211

1212+
@property
1213+
def local_storage(self) -> WebStorage:
1214+
return self._local_storage
1215+
1216+
@property
1217+
def session_storage(self) -> WebStorage:
1218+
return self._session_storage
1219+
12091220
def _close_error_with_reason(self) -> TargetClosedError:
12101221
return TargetClosedError(
12111222
self._close_reason or self._browser_context._effective_close_reason()

playwright/_impl/_screencast.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pathlib import Path
1717
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union
1818

19-
from playwright._impl._api_structures import ScreencastFrame
19+
from playwright._impl._api_structures import ScreencastFrame, ScreencastSize
2020
from playwright._impl._artifact import Artifact
2121
from playwright._impl._connection import from_nullable_channel
2222
from playwright._impl._disposable import DisposableStub
@@ -36,6 +36,10 @@
3636
"top-left",
3737
"top-right",
3838
]
39+
ScreencastCursor = Literal[
40+
"none",
41+
"pointer",
42+
]
3943

4044

4145
class Screencast:
@@ -58,6 +62,7 @@ def _dispatch_frame(self, params: dict) -> None:
5862
result = self._on_frame(
5963
{
6064
"data": data,
65+
"timestamp": params.get("timestamp", 0),
6166
"viewportWidth": params["viewportWidth"],
6267
"viewportHeight": params["viewportHeight"],
6368
}
@@ -70,6 +75,7 @@ async def start(
7075
onFrame: ScreencastFrameCallback = None,
7176
path: Union[str, Path] = None,
7277
quality: int = None,
78+
size: ScreencastSize = None,
7379
) -> DisposableStub:
7480
if self._started:
7581
raise Error("Screencast is already started")
@@ -79,6 +85,7 @@ async def start(
7985
"screencastStart",
8086
None,
8187
{
88+
"size": size,
8289
"quality": quality,
8390
"sendFrames": bool(onFrame),
8491
"record": bool(path),
@@ -104,6 +111,7 @@ async def show_actions(
104111
duration: float = None,
105112
position: ScreencastPosition = None,
106113
fontSize: int = None,
114+
cursor: ScreencastCursor = None,
107115
) -> DisposableStub:
108116
await self._page._channel.send(
109117
"screencastShowActions", None, locals_to_params(locals())

0 commit comments

Comments
 (0)