Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .azure-pipelines/publish-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ extends:
- task: NuGetAuthenticate@1
displayName: "Authenticate NuGet to DevDiv_PublicPackages feed"

# Playwright.Tooling fetches playwright-core with `npm pack`, which picks up
# the registry and credentials from this .npmrc.
# Playwright.Tooling builds the driver from .upstream/ with `npm ci`, which
# picks up the registry and credentials from this .npmrc.
- task: UseNode@1
displayName: "Install Node.js"
inputs:
Expand Down Expand Up @@ -97,8 +97,8 @@ extends:
echo '{ "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json
sudo service docker start

- bash: ./build.sh --download-driver
displayName: "Download the driver"
- bash: ./build.sh --build-driver
displayName: "Build the driver"

- task: AzureCLI@2
displayName: "Login to ACR via OIDC"
Expand Down
11 changes: 6 additions & 5 deletions .azure-pipelines/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ extends:
displayName: 'Use DevDiv_PublicPackages as the only NuGet source'
- task: NuGetAuthenticate@1
displayName: 'Authenticate NuGet to DevDiv_PublicPackages feed'
# Playwright.Tooling fetches playwright-core with `npm pack`, which picks up
# the registry and credentials from this .npmrc.
# Playwright.Tooling builds the driver from .upstream/ with `npm ci`, which
# picks up the registry and credentials from this .npmrc.
- task: UseNode@1
displayName: 'Install Node.js'
inputs:
Expand All @@ -105,12 +105,13 @@ extends:
displayName: 'Authenticate npm to DevDiv_PublicPackages feed'
inputs:
workingFile: .npmrc
# We need to download the driver first, so we can build
# We need to build the driver first, so we can build. This clones
# microsoft/playwright into .upstream/ at DriverVersion of src/Common/Version.props.
- task: DotNetCoreCLI@2
displayName: Download the driver
displayName: Build the driver
inputs:
command: 'run'
arguments: '--project $(Build.SourcesDirectory)/src/tools/Playwright.Tooling/Playwright.Tooling.csproj -- download-drivers --basepath $(Build.SourcesDirectory)'
arguments: '--project $(Build.SourcesDirectory)/src/tools/Playwright.Tooling/Playwright.Tooling.csproj -- build-driver --basepath $(Build.SourcesDirectory)'
- task: DotNetCoreCLI@2
displayName: Build Playwright NuGet Packages
inputs:
Expand Down
14 changes: 13 additions & 1 deletion .claude/skills/playwright-dotnet-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ Use this skill once the roll PR has merged into `main` and the upstream JS `v1.X

Throughout this doc, replace `X` with the minor version (e.g. `64` for `1.64.0`) and `<user>` with the fork owner (`gh api user --jq .login`).

Check that the driver is rolled. `DriverVersion` in `src/Common/Version.props` on `main` must be a commit of the upstream `release-1.X` branch: the tag `v1.X.0`, or the tip of the branch, as the user says. The driver reports the version of `playwright-core` at that commit, so it must be `1.X.0`:

```bash
./build.sh --build-driver
pin=$(git -C .upstream rev-parse HEAD)
git -C .upstream fetch origin release-1.X
git -C .upstream merge-base --is-ancestor "$pin" FETCH_HEAD && echo "on release-1.X"
git -C .upstream show "$pin":packages/playwright-core/package.json | grep '"version"'
```

If it is not, roll first (`./build.sh --roll v1.X.0`, or `--roll release-1.X`).

The full release checklist lives in the private `microsoft/playwright-internal` repo as the `v1.X release checklist` issue. Find its number once:

```bash
Expand Down Expand Up @@ -51,7 +63,7 @@ git show upstream/release-1.X:src/Common/Version.props | grep AssemblyVersion

## 3. Release notes

The .NET notes in `docs/src/release-notes-csharp.md` are written upstream together with the Node.js notes, following the `playwright-release-notes` skill (`../playwright/.claude/skills/playwright-release-notes/SKILL.md`). Assume the `## Version 1.X` section is already on `release-1.X`, and only add what's missing: significant changes that only the .NET port has, such as a new NUnit, MSTest or xUnit integration feature or a .NET-only API. Leave out minor bug fixes. To find candidates, list this repo's changes since the previous release branch was cut:
The .NET notes in `docs/src/release-notes-csharp.md` are written upstream together with the Node.js notes, following the `playwright-release-notes` skill (`.claude/skills/playwright-release-notes/SKILL.md` on `main` of `microsoft/playwright`). Assume the `## Version 1.X` section is already on `release-1.X`, and only add what's missing: significant changes that only the .NET port has, such as a new NUnit, MSTest or xUnit integration feature or a .NET-only API. Leave out minor bug fixes. To find candidates, list this repo's changes since the previous release branch was cut:

```bash
git log --oneline --no-merges $(git merge-base upstream/release-1.<X-1> upstream/main)..upstream/main | grep -E '^\w+ (feat|fix)'
Expand Down
56 changes: 30 additions & 26 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,57 @@ Help the user roll to a new version of Playwright.
../../../ROLLING.md contains general instructions and scripts.
See also [CLAUDE.md](../../../CLAUDE.md) for build, test, and architecture details.

Unless the exact version is specified by the user, you need to find the latest version of the driver.
Check the publish workflow here: https://github.com/microsoft/playwright/actions/workflows/publish_release.yml. The step that builds and publishes the driver contains the exact version you need.
`DriverVersion` in `src/Common/Version.props` holds one `microsoft/playwright` commit. `.upstream/` (gitignored) is a full clone of `microsoft/playwright` at that commit. The driver is built from it, the API and the transport channels are generated from it, and the hand-written client ports its JS client. A roll moves `DriverVersion` from the old commit to the new one, and ports the diff between the two. Do not commit, branch or edit in `.upstream/`.

Now, with the driver version known, always start with running the roll script to update the version and regenerate the API to see the state of things.
Roll to the ref that the user names: a branch (`main`, `release-1.66`), a tag (`v1.66.0`) or a commit. Without one, roll to `main`.

Always start with running the roll script, to see the state of things. It writes the commit to `DriverVersion`, checks it out in `.upstream/`, builds the driver from it and regenerates the API. It needs git and Node.js with npm.

```bash
./build.sh --roll <driver-version>
./build.sh --roll <ref>
old=$(git show HEAD:src/Common/Version.props | sed -n 's|.*<DriverVersion>\(.*\)</DriverVersion>.*|\1|p')
new=$(git -C .upstream rev-parse HEAD)
```

Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones.
The driver reports the version of `packages/playwright-core/package.json` at the new commit: `1.66.0` on a release tag, `1.67.0-next` on `main`. The script prints it.

Afterwards, walk through the upstream changes that affect the .NET client and port the relevant ones.

## Determining what to port

List the upstream commits that touched a client-relevant path since the last roll. The paths cover everything that can change the public Java surface or the wire protocol:
List the upstream commits between `$old` and `$new` that touched a client-relevant path. The paths cover everything that can change the public .NET surface or the wire protocol:

- `docs/src/api/` — the source of truth for `api.json`. Method/option additions, removals, and `langs:` filter changes flow from here.
- `packages/playwright-core/src/client/` — the JS client implementation that the Java client mirrors.
- `packages/playwright-core/src/client/` — the JS client implementation that the .NET client mirrors.
- `packages/isomorphic/` — selector engines, locator generation/parsing, and aria-snapshot logic shared between client and server. Changes here can affect client-side helpers like `getByRoleSelector`.
- `packages/playwright/src/matchers/matchers.ts` — assertion-method definitions. Changes here usually correspond to new options on `LocatorAssertions` / `PageAssertions`.
- `packages/protocol/spec/**.yml` — the wire protocol schema. Method/event additions, parameter renames, and result-shape changes affect what the .NET implementation classes need to send/receive.

```bash
cd ~/playwright
PREV_TAG=$(git tag | grep -E '^v1\.[0-9]+\.[0-9]+$' | sort -V | tail -1) # e.g. v1.59.1
git log "$PREV_TAG"..HEAD --oneline -- \
'docs/src/api/' \
'packages/playwright-core/src/client/' \
'packages/isomorphic/' \
'packages/playwright/src/matchers/matchers.ts' \
'packages/protocol/spec/**.yml'
./build.sh --changes # since DriverVersion at HEAD, oldest first
./build.sh --changes <from> # or since another commit or tag, e.g. v1.65.0
```

Walk that list top-to-bottom (oldest-first is easier — newest is at top, so reverse). For each commit:
1. Read the commit (`git show <sha>`) to see what client/protocol/docs changed.
Walk that list top-to-bottom. For each commit:
1. Read the commit (`git -C .upstream show <sha>`) to see what client/protocol/docs changed.
2. If it's JS-internal (bundling, dispatcher conventions, electron, mcp, dashboard, trace-viewer, test-runner) — skip.
3. If it touches `docs/src/api/` or types, check `langs:` annotations — features marked with languages other than "csharp" do not apply.
4. If it adds/changes a public API method or option that applies to .NET, port it. The 'build.sh --roll' already regenerated the types/options, so we usually only need to update the implementation.
5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in HEAD before porting.
5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in `.upstream/` before porting.
6. Maintain a running notes file (e.g. `/tmp/roll-notes.md`) listing each upstream PR as ported / skipped / verified-already-supported, with a one-line reason. This file becomes the body of the eventual PR.
7. Do not forget to port tests that cover .NET-relevant changes. At least one test per every API method/option to excercise the new code paths. Some server-side implementation tests are not needed in this port.

## Mimicking the JavaScript implementation

The .NET client is a port of the JS client in `../playwright/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic:
The .NET client is a port of the JS client in `.upstream/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic:

```
../playwright/packages/playwright-core/src/client/browserContext.ts
../playwright/packages/playwright-core/src/client/page.ts
.upstream/packages/playwright-core/src/client/browserContext.ts
.upstream/packages/playwright-core/src/client/page.ts
...
```

**Read the final state at the release tag, not just the introducing PR diff.** When a feature evolves across multiple PRs (e.g. a method was introduced in one PR, then some options were added in follow-ups, then some logic was changed), the original PR's diff is a misleading reference — the final shape lives in `git show v1.X.0:packages/playwright-core/src/client/foo.ts`. Always read the v1.X.0 file before porting non-trivial logic.
**Read the final state in `.upstream/`, not just the introducing PR diff.** When a feature evolves across multiple PRs (e.g. a method was introduced in one PR, then some options were added in follow-ups, then some logic was changed), the original PR's diff is a misleading reference — the final shape lives in `.upstream/packages/playwright-core/src/client/foo.ts`, which is at the new commit. Always read that file before porting non-trivial logic. `git -C .upstream diff "$old" "$new" -- <path>` shows everything that changed in one file.

Key translation rules:

Expand All @@ -68,7 +67,7 @@ Key translation rules:

**Watch for channel migrations.** A method that lived on `BrowserContext` may move to `Tracing` (and vice-versa) without notice — the protocol spec is the source of truth. If a `SendMessageToServerAsync("foo", ...)` call suddenly fails, grep `packages/protocol/spec/*.yml` to find which channel actually owns `foo` now.

**Wire format changes can be subtle.** A method's result shape can change without renaming anything. These are easy to miss when walking client.ts because the JS code just adapts; the regression only surfaces when an existing test's deserialization breaks. Always diff `packages/protocol/spec/*.yml` for each commit on the walk list, not just the client code.
**Wire format changes can be subtle.** A method's result shape can change without renaming anything. These are easy to miss when walking client.ts because the JS code just adapts; the regression only surfaces when an existing test's deserialization breaks. Always diff `packages/protocol/spec/*.yml` for each commit on the walk list, not just the client code. `git -C .upstream diff "$old" "$new" -- packages/protocol/spec` shows the whole change of the protocol.

**Update tests similarly to upstream.** When upstream removes or modifies a test, apply similar changes.

Expand All @@ -84,7 +83,9 @@ In the docs markdown file (e.g. `docs/src/api/class-browser.md`), add `alias` (a
+ - alias-csharp: BrowserBindResult
```

Use `alias-csharp` (in addition to bare `alias`) when upstream already declared a different `alias` for other languages and you only want to override .NET's name — typically to preserve a name we've already shipped and don't want to break. After making this change, re-run `./build.sh --roll <version>` to regenerate, then update any hand-written implementation code to use the new type name. The upstream docs change must also land in `microsoft/playwright` via a separate PR or it will revert on the next roll.
Use `alias-csharp` (in addition to bare `alias`) when upstream already declared a different `alias` for other languages and you only want to override .NET's name — typically to preserve a name we've already shipped and don't want to break.

The generated files come from `.upstream/` at `DriverVersion`, and CI fails when they differ, so the docs change must land in `microsoft/playwright` first. Make it in another checkout of `microsoft/playwright` (not in `.upstream/`), and try it with `PW_SRC_DIR=<that checkout> ./build.sh --generate`. Once the upstream PR has merged, roll to a commit that has it, then update any hand-written implementation code to use the new type name.

## Running the full test suite

Expand All @@ -100,6 +101,9 @@ tail -5 /tmp/test-results.txt # summary
Run as a background task — don't block the conversation on it. For each failure: check whether it's a flake (port collision, browser timing) by running the single test in isolation, then investigate whether your roll introduced it or it's pre-existing.

## Tips & Tricks
- Project checkouts are in the parent directory (`../`).
- `.upstream/` is the upstream checkout, at the new commit. Only read from it.
- Use the "gh" cli to interact with GitHub.
- When running git commands against an upstream repo, always use `git -C /path/to/repo <subcommand>` instead of `cd /path/to/repo && git <subcommand>`.
- When running git commands against the upstream repo, always use `git -C .upstream <subcommand>` instead of `cd .upstream && git <subcommand>`.
- `.upstream/ is not at <commit>`: run `./build.sh --build-driver`. It checks out `DriverVersion` and builds the driver again when the commit changed.
- `.upstream has local changes`: see them with `git -C .upstream status`, and discard them.
- `./build.sh --generate` regenerates the API and the transport channels without moving `DriverVersion`.
18 changes: 11 additions & 7 deletions .claude/skills/playwright-triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ outcome that this repo adds, an upstream bug report.
## The checkouts

`$REPO_DIR` is the checkout of `microsoft/playwright-dotnet` that the guide describes. The driver is
downloaded (`./build.sh --download-driver`), the solution is built (`dotnet build ./src`), and
built (`./build.sh --build-driver`), the solution is built (`dotnet build ./src`), and
Chromium, Firefox and WebKit are installed
(`pwsh src/Playwright/bin/Debug/netstandard2.0/playwright.ps1 install --with-deps`). It is the
working directory, and the checkout that the guides refer to. The clone is shallow: run
Expand All @@ -26,12 +26,16 @@ working directory, and the checkout that the guides refer to. The clone is shall
`$PLAYWRIGHT_DIR` is a shallow clone of `microsoft/playwright` at `main`. The .NET client is a port
of the JavaScript client in `packages/playwright-core/src/client/`. The API docs are in
`docs/src/api/`, the API generator is `utils/doclint/generateDotnetApi.js`, and the upstream tests
are in `tests/page/` and `tests/library/`. The clone is not at the commit of the driver:
`<DriverVersion>` in `src/Common/Version.props` names that version, `main` is close to it, and a
release has the tag `v<DriverVersion>`. The clone is not built. Before you run an upstream test in
it, run `npm ci`, `npm run build` and `npx playwright install` there. Then `npm run ctest <spec>`,
are in `tests/page/` and `tests/library/`. The clone is not at the commit of the driver, but `main`
is close to it. The clone is not built. Before you run an upstream test in it, run `npm ci`,
`npm run build` and `npx playwright install` there. Then `npm run ctest <spec>`,
`npm run ftest <spec>` and `npm run wtest <spec>` run one spec file on Chromium, Firefox and WebKit.

`$REPO_DIR/.upstream` is a full clone of `microsoft/playwright` at the commit of the driver, which
`<DriverVersion>` in `src/Common/Version.props` names. The driver is built from it, and the API is
generated from it. Read the code of the driver there, and use its history and tags. Do not commit,
branch or edit in it: write and run upstream tests in `$PLAYWRIGHT_DIR`.

## Published versions

To run a published version, for example to bisect, create
Expand All @@ -46,7 +50,7 @@ dotnet run
Put the repro in `Program.cs` as top-level statements, as `src/Playwright.Examples/Program.cs`
does. `playwright.ps1 install` fetches the browsers of that version. Match the change to commits in
`$REPO_DIR` with `git log v<good>..v<bad> -- src/Playwright/`, and to the JavaScript client in
`$PLAYWRIGHT_DIR` with the same tags.
`$REPO_DIR/.upstream` with the same tags.

## Upstream bugs

Expand Down Expand Up @@ -94,7 +98,7 @@ Write the rest of the comment as the note: the verdict, the evidence, and that t
## PRs from third parties

Do not run a PR that changes a `.csproj`, `.props` or `.sln` file, `nuget.config`, `build.sh`,
`src/tools/Playwright.Tooling/` (the driver download), `src/Playwright/build/` (MSBuild targets that
`src/tools/Playwright.Tooling/` (the driver build), `src/Playwright/build/` (MSBuild targets that
run on every build), `.github/`, `.azure-pipelines/`, `utils/`, `package.json`, `package-lock.json`,
or the generated files under `src/Playwright/API/Generated/` and
`src/Playwright/Transport/Protocol/Generated/`.
4 changes: 2 additions & 2 deletions .github/actions/setup-triage-runner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ runs:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
- name: Download the driver and build
- name: Build the driver and the solution
shell: bash
run: |
./build.sh --download-driver
./build.sh --build-driver
dotnet build ./src
- name: Install the browsers
shell: bash
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/code-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ jobs:
dotnet-version: |
8.0.x
10.0.x
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
- name: Check formatting
run: dotnet format ./src/ --verify-no-changes -v:diag
- name: Download driver
run: ./build.sh --download-driver
- name: Build the driver
run: ./build.sh --build-driver
- name: Check that the generated files are up to date
run: |
./build.sh --generate
git diff --exit-code
- name: Build projects (runs .NET analyzers)
run: dotnet build ./src
Loading
Loading