## 1. Project Overview & Quickstart (microsoft/playwright-python)

## File: README.md

# 🎭 [Playwright](https://playwright.dev) for Python [](https://pypi.python.org/pypi/playwright/) [](https://aka.ms/playwright/discord)

Playwright is a Python library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) browsers with a single API. Playwright delivers automation that is **ever-green**, **capable**, **reliable** and **fast**. [See how Playwright is better](https://playwright.dev/python).

|          | Linux | macOS | Windows |
|   :---   | :---: | :---: | :---:   |
| Chromium 156.0.8078.12 | ✅ | ✅ | ✅ |
| WebKit 27.2 | ✅ | ✅ | ✅ |
| Firefox 157.0 | ✅ | ✅ | ✅ |

## Documentation

[https://playwright.dev/python/docs/intro](https://playwright.dev/python/docs/intro)

## API Reference

[https://playwright.dev/python/docs/api/class-playwright](https://playwright.dev/python/docs/api/class-playwright)

## Example

```py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    for browser_type in [p.chromium, p.firefox, p.webkit]:
        browser = browser_type.launch()
        page = browser.new_page()
        page.goto('http://playwright.dev')
        page.screenshot(path=f'example-{browser_type.name}.png')
        browser.close()
```

```py
import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        for browser_type in [p.chromium, p.firefox, p.webkit]:
            browser = await browser_type.launch()
            page = await browser.new_page()
            await page.goto('http://playwright.dev')
            await page.screenshot(path=f'example-{browser_type.name}.png')
            await browser.close()

asyncio.run(main())
```

## Other languages

More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in
- [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro),
- [.NET](https://playwright.dev/dotnet/docs/intro),
- [Java](https://playwright.dev/java/docs/intro).

---

## File: AGENTS.md

# AGENTS.md

Guidance for coding agents working in this repository.

## What this is

Python bindings for [Playwright](https://playwright.dev). The Python client talks JSON over a pipe to the Node-based driver bundled in `playwright/driver/`. The pipe protocol is defined upstream in `packages/protocol/spec/*.yml`.

## Layout

- `playwright/_impl/` — hand-written client implementation (one module per object: `_browser.py`, `_page.py`, `_locator.py`, `_network.py`, etc.). Edit these to add or change behavior. Internal types are in `_internal_structures.py`.
- `playwright/async_api/_generated.py`, `playwright/sync_api/_generated.py`, and the public TypedDicts in `playwright/_impl/_api_structures.py` — **auto-generated** from the upstream API docs. Never edit by hand; rerun `./scripts/update_api.sh` after a roll or after changing `scripts/codegen/`.
- `scripts/codegen/generate_api.js` — the generator. A Node.js script that loads the upstream doc parser (`utils/doclint/api_parser.js`) from `$PW_SRC_DIR`, filters the docs for Python, and writes the whole public API: signatures, types, docstrings, event overloads, the calls into `_impl/`, and the TypedDicts. It does not read `_impl/`.
- `scripts/codegen/python_rules.js` — the Python facts that the docs do not contain (positional options, struct `total=`, hidden members and options). Policy: the shipped Python API wins; each entry is a candidate for an upstream docs fix, and the generator fails on entries that no longer match anything. Every entry needs a one-line rationale comment above it.
- `playwright/_impl/_protocol.py`, `_protocol_types.py` and `_protocol_channels.py` — **auto-generated** by `scripts/codegen/generate_protocol.py` (run by `./scripts/update_api.sh`) from the upstream protocol spec.
  - `_protocol_channels.py` has a typed channel class for each interface (`PageChannel`, `FrameChannel`, …), like upstream's `channels.d.ts`. Each command is an async method with the protocol name and parameters (`await self._channel.goto(url=..., waitUntil=..., referer=..., timeout=...)`); each event has `on<Event>` / `off<Event>` methods. A result or event payload has every top-level field; a field that the driver left out is `None`.
  - `_protocol_types.py` has the TypedDicts for initializers, results, events and nested types, named as upstream's `utils/generate_channels.js` names them (`PageInitializer`, `FrameGotoResult`, `PageRouteEvent`, …).
  - `_protocol.py` has the parameters of every command. With `PW_CHECK_PROTOCOL=1`, `playwright/_impl/_protocol_check.py` checks each message that the client sends against it. The test suite sets this, and fails a test that sends an unknown command, an unknown or missing parameter, or a value of the wrong type.
- `scripts/codegen/compare_api.py` — compares two generated files (signatures, annotations, call bodies, docstrings) and lists every difference. Use it to check that a generator change does not change the API.
- `tests/async/`, `tests/sync/` — pytest suites, ported from the upstream tests. Most new tests are added to the async file with a sync mirror. See [Tests](#tests).
- `DRIVER_VERSION` — the single source of truth for which Playwright release the driver is assembled from (one line, the `playwright-core` npm version, e.g. `1.61.0`, no `v` prefix). Read by `setup.py`, `scripts/build_driver.py`, and CI. The wheel build downloads `playwright-core` at this version from npm plus the matching Node.js binary and assembles the per-platform bundles — no source build. The version is baked into the staged bundle filenames (`driver/playwright-<version>-<suffix>.zip`), so it doubles as the build cache key.
- `NODE_VERSION` — the Node.js version bundled with the driver (one line, e.g. `24.16.0`). Maintained at roll time by `scripts/update_node_version.py` (latest LTS, mirroring upstream's `utils/build/update-playwright-node.mjs`).
- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Fetches `playwright-core` with `npm pack` (needs Node.js/npm on PATH; honours a root `.npmrc`) and the Node.js binaries over plain HTTP; invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six).
- The API docs are **not** shipped in the bundle. `scripts/update_api.sh` reads them from a separate `microsoft/playwright` checkout (`$PW_SRC_DIR`) on the exact release branch for `DRIVER_VERSION`, never from a checkout on `main` or a feature branch. Needed only when regenerating the API, never at runtime.
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.

## Setup

`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js and npm for the driver build):

```sh
python3 -m venv env && source env/bin/activate
pip install --upgrade pip
pip install -r local-requirements.txt
pip install -e .
python -m build --wheel        # downloads playwright-core @ DRIVER_VERSION + Node.js and assembles the driver
pre-commit install
```

If the system lacks `python3-venv`, `uv venv env` is an acceptable substitute (then `uv pip install --python env/bin/python --upgrade pip`).

## Common commands

- Regenerate `_generated.py`: `./scripts/update_api.sh` (runs codegen + pre-commit on the generated files).
- Lint everything: `pre-commit run --all-files`.
- Type-check: `mypy playwright`.
- Run tests: `pytest --browser chromium [-k name]`. Browsers are installed via `playwright install chromium` (do **not** use `--with-deps`, which requires sudo).

The public API comes from the upstream docs. After `./scripts/update_api.sh`, run `mypy playwright`: the generated classes type their `_impl_obj`, so every new member or option that `_impl/` does not implement yet is a mypy error in `_generated.py`. Fix it in `_impl/`, or record a justified difference in `scripts/codegen/python_rules.js` — not by hand-editing `_generated.py`.

## Tests

The tests are a port of the upstream tests in `microsoft/playwright` (`tests/page/`, `tests/library/`). Keep them close to upstream, so that each upstream test has one place in this repo.

**First rule: follow the upstream names and files.**

- When a change needs a test, find the upstream test for the same behavior and port it. Do not write a new test when an upstream test exists.
- Put the test in the Python file that matches the upstream file: `tests/page/expect-misc.spec.ts` becomes `tests/async/test_expect_misc.py`. If that file does not exist, create it.
- Name the test after the upstream title, in snake_case, with the `describe` title in front: `test('should support pseudo element')` in `test.describe('toHaveCSS')` becomes `test_to_have_css_should_support_pseudo_element`.
- Add the sync mirror in `tests/sync/`, with the same file name and test name.

**Second rule: keep the number of tests small.** This applies to every test, and most of all to Python-only behavior that has no upstream test (the sync API, asyncio, the driver).

- **Extend before you add**: before you write a new test, look for an existing test that covers the same feature or code path. If one exists, add the new edge case or assertion to it. Create a new test only when no existing test is a good fit, or when the new scenario needs a different setup.
- **New features**: cover the mainstream scenario and the important edge cases only. Do not create matrices of tests for every scenario, option, or aspect.
- **Regressions**: generalize the provided regression cases into meaningful tests, and stop there. Do not add more tests around them.

## Rolling Playwright to a new version

This is the recurring high-stakes task. Use the dedicated skill:

→ **[`.claude/skills/playwright-roll/SKILL.md`](.claude/skills/playwright-roll/SKILL.md)**

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.
- Don't add `# type: ignore` or modify `_generated.py` to silence pyright; fix the source of the mismatch.
- New public methods on impl classes need a sync test mirror under `tests/sync/`.
- Keep `scripts/codegen/python_rules.js` minimal — every entry needs a one-line rationale comment above it. Prefer fixing the upstream docs.
- Implementation methods take the Python (snake_case) names that the generated API passes. Their options have no default: an option that can be absent is `Optional[X]` without `= None`. So mypy reports each caller that does not pass a new option.
- Send messages through the generated channel methods, never by name: `await self._channel.click(selector=selector, ..., clickCount=click_count, timeout=self._timeout(timeout))`. Pass every parameter, with the protocol (camelCase) name; an absent option is `None`, and the connection drops it. Declare the channel type on each ChannelOwner subclass (`_channel: "PageChannel"`). Listen to events with `self._channel.on<Event>(handler)`.

## 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 <changed-files>
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
- <describe the change very! briefly>

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.

---

## File: CODE_OF_CONDUCT.md

# Microsoft Open Source Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).

Resources:

- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns

---

## File: CONTRIBUTING.md

# Contributing

## Choosing an Issue

To maintain project quality and focus, Playwright **requires a corresponding issue** for every contribution, with the exception of minor documentation fixes.

If you would like to address a bug or feature that isn't currently listed, **please file a new issue first**. This allows the community and maintainers to provide early feedback and facilitates a discussion before you invest time in developing a pull request.

When submitting an issue, please state clearly if you intend to work on it. Once triaged and approved, the maintainers will determine the best path forward—whether the task should be handled by the **core team**, an **automated agent**, or a **community contributor**. If the issue is assigned to you, you may then proceed with your changes and submit a PR.

### Submission Policy
To ensure the maintainability of the project, please note the following:

* **Unsolicited PRs:** Pull requests submitted without a linked issue or prior approval will be closed.
* **Low-Quality AI Contributions:** PRs that do not meet our quality standards or lack human oversight (including low-quality agentic submissions) will be closed without explanation.
* **Approval Required:** Only proceed with a PR once the issue has been officially assigned to you or approved for community contribution.

## How to Contribute

### Configuring python environment

The project development requires Python version 3.10+. To set it as default in the environment run the following commands:

```sh
# You may need to install python 3.10 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.10-venv`
python3.10 -m venv env
source ./env/bin/activate
```

Install required dependencies:

```sh
python -m pip install --upgrade pip
pip install -r local-requirements.txt
```

Build and install drivers:

The driver is assembled from published artifacts — the `playwright-core` npm
package (version pinned in `DRIVER_VERSION`) and the official Node.js binary
(pinned in `NODE_VERSION`). Building a wheel downloads them with `npm pack` and
plain HTTP, so Node.js/npm must be installed; no git or source build is needed.

```sh
pip install -e .
python -m build --wheel
```

Install the browsers to test against:

```sh
playwright install chromium
```

Run tests:

```sh
pytest --browser chromium
```

Checking for typing errors

```sh
mypy playwright
```

Format the code

```sh
pre-commit install
pre-commit run --all-files
```

For more details look at the [CI configuration](./.github/workflows/ci.yml).

### Regenerating APIs

`update_api.sh` generates `playwright/{async,sync}_api/_generated.py` and the
public TypedDicts in `playwright/_impl/_api_structures.py` from the API docs in
a separate `microsoft/playwright` checkout on the exact release branch for
`DRIVER_VERSION` (for example `release-1.64`); point `PW_SRC_DIR` at it. The
generator is
`scripts/codegen/generate_api.js`; Python facts that the docs do not contain
live in `scripts/codegen/python_rules.js`.

```bash
PW_SRC_DIR=../playwright-for-python ./scripts/update_api.sh
pre-commit run --all-files
```

## Contributor License Agreement

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.

## Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

---

## File: local-requirements.txt

asyncio-atexit==1.0.1
autobahn==24.4.2
build==1.6.1
mypy==2.4.0
objgraph==3.6.2
Pillow==12.3.0
pixelmatch==0.3.0
pre-commit==3.5.0
pyOpenSSL==26.0.0
pytest==9.0.3
pytest-asyncio==1.4.0
pytest-rerunfailures==16.7
pytest-timeout==2.4.0
PyYAML==6.0.3
service_identity==24.2.0
twisted==25.5.0
types-pyOpenSSL==24.1.0.20240722
types-PyYAML==6.0.12.20260906

---

## File: requirements.txt

# This file was autogenerated by uv via the following command:
#    uv pip compile pyproject.toml -o requirements.txt
greenlet==3.5.6
    # via playwright (pyproject.toml)
pyee==13.0.1
    # via playwright (pyproject.toml)
typing-extensions==4.16.0
    # via pyee

---

## File: ROLLING.md

# Rolling Playwright-Python to the latest Playwright driver

* checkout repo: `git clone https://github.com/microsoft/playwright-python`
* make sure local python is 3.10 or newer
    * create virtual environment, if don't have one: `python -m venv env`
* activate venv: `source env/bin/activate`
* install all deps:
```
python -m pip install --upgrade pip
pip install -r local-requirements.txt
pre-commit install
pip install -e .
```
* change the driver pin in `DRIVER_VERSION` (the `playwright-core` npm version, e.g. `1.61.0`) and refresh `NODE_VERSION`: `python scripts/update_node_version.py`
* download the new driver: `python -m build --wheel` (fetches `playwright-core` via `npm pack` + the matching Node.js binary and assembles the bundle; no source build).
* generate API from the docs. This needs a separate `microsoft/playwright` checkout used only for rolls, on the exact release branch for the new version (e.g. `release-1.64`), clean and up to date with `origin`: `PW_SRC_DIR=../playwright-for-python ./scripts/update_api.sh`
* run `mypy playwright`: every error in `_generated.py` points at an implementation method in `playwright/_impl/` that lacks a new member or option. Implement it there, or record the difference in `scripts/codegen/python_rules.js`.
* commit changes & send PR
* wait for bots to pass & merge the PR

---

## File: SECURITY.md

## Security

Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).

If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.

## Reporting Security Issues

**Please do not report security vulnerabilities through public GitHub issues.**

Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).

If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).

You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).

Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:

  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
  * Full paths of source file(s) related to the manifestation of the issue
  * The location of the affected source code (tag/branch/commit or direct URL)
  * Any special configuration required to reproduce the issue
  * Step-by-step instructions to reproduce the issue
  * Proof-of-concept or exploit code (if possible)
  * Impact of the issue, including how an attacker might exploit the issue

This information will help us triage your report more quickly.

If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.

## Preferred Languages

We prefer all communications to be in English.

## Policy

Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).

---

## File: SUPPORT.md

# Support

## How to file issues and get help

This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.

For help and questions about using this project, please see the [docs site for Playwright for Python][docs].

Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.

## Microsoft Support Policy

Support for Playwright for Python is limited to the resources listed above.

[gh-issues]: https://github.com/microsoft/playwright-python/issues/
[docs]: https://playwright.dev/python/
[discord-server]: https://aka.ms/playwright/discord

---

## File: .pre-commit-config.yaml

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
        exclude: tests/assets/har-sha1-main-response.txt
      - id: check-yaml
      - id: check-toml
      - id: requirements-txt-fixer
      - id: check-ast
      - id: check-builtin-literals
      - id: check-executables-have-shebangs
      - id: check-merge-conflict
  - repo: https://github.com/psf/black
    rev: 25.1.0
    hooks:
      - id: black
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.17.0
    hooks:
      - id: mypy
        additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-PyYAML==6.0.12.20260906]
  - repo: https://github.com/pycqa/flake8
    rev: 7.3.0
    hooks:
      - id: flake8
  - repo: https://github.com/pycqa/isort
    rev: 6.0.1
    hooks:
      - id: isort
  - repo: local
    hooks:
      - id: pyright
        name: pyright
        entry: pyright
        language: node
        pass_filenames: false
        types: [python]
        additional_dependencies: ["pyright@1.1.403"]
  - repo: local
    hooks:
      - id: check-license-header
        name: Check License Header
        entry: ./utils/linting/check_file_header.py
        language: python
        types: [python]

---

## File: examples/todomvc/requirements.txt

pytest-playwright

---

## File: tests/assets/file-to-upload-2.txt

contents of the file

---

## File: tests/assets/file-to-upload.txt

contents of the file

---

## File: tests/assets/har-sha1-main-response.txt

Hello, world

---

## File: tests/assets/simple.json

{"foo": "bar"}

---

## File: tests/assets/client-certificates/README.md

# Client Certificate test-certificates

Regenerate all certificates by running:

```
bash generate.sh
```

---

## File: tests/assets/extension-mv3-simple/manifest.json

{
  "name": "Simple extension",
  "version": "0.1",
  "background": {
    "service_worker": "index.js"
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "css": [],
    "js": ["content-script.js"]
  }],
  "permissions": ["background", "activeTab"],
  "manifest_version": 3
}

---

## File: tests/assets/extension-mv3-with-logging/manifest.json

{
    "manifest_version": 3,
    "name": "Console Log Extension",
    "version": "1.0",
    "background": {
      "service_worker": "background.js"
    },
    "permissions": [
      "tabs"
    ],
    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"]
      }
    ]
  }

---

## File: .azure-pipelines/publish-docker.yml

# Trigger: any `v*` release tag (e.g. v1.40.0).
# Can also be queued manually from the ADO UI.
trigger:
  tags:
    include:
      - v*

pr: none

parameters:
- name: releaseChannel
  displayName: "IMPORTANT: set this to 'canary' when triggering manually"
  type: string
  default: stable
  values:
  - stable
  - canary

resources:
  repositories:
  - repository: 1esPipelines
    type: git
    name: 1ESPipelineTemplates/1ESPipelineTemplates
    ref: refs/tags/release

extends:
  template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
  parameters:
    pool:
      name: DevDivPlaywrightAzurePipelinesUbuntu2204
      os: linux
    sdl:
      sourceAnalysisPool:
        name: DevDivPlaywrightAzurePipelinesWindows2022
        # The image must be windows-based due to restrictions of the SDL tools. See: https://aka.ms/AAo6v8e
        os: windows
      suppression:
        suppressionFile: $(Build.SourcesDirectory)\.azure-pipelines\guardian\SDL\.gdnsuppress
    stages:
    - stage: Publish
      jobs:
      - job: PublishDocker
        displayName: "Publish Docker images to ACR"
        # arm64 images are cross-built under QEMU emulation, which is slow.
        timeoutInMinutes: 360
        steps:
        # Full history and tags: setuptools-scm derives the package version
        # (and thus the image tags) from the release tag.
        - checkout: self
          fetchDepth: 0
          fetchTags: true
          displayName: "Checkout code"

        - task: UsePythonVersion@0
          inputs:
            versionSpec: '3.10'
          displayName: "Use Python"

        # Resolve pip installs through the DevDiv_PublicPackages feed. The task exports
        # an authenticated PIP_INDEX_URL, which build.sh also forwards to the image builds.
        - task: PipAuthenticate@1
          inputs:
            artifactFeeds: DevDiv/DevDiv_PublicPackages
          displayName: "Authenticate pip to DevDiv_PublicPackages feed"

        - task: UseNode@1
          inputs:
            version: '24.x'
          displayName: "Install Node.js"

        # Relocate the Docker data-root to the large /mnt volume: this job builds
        # 6 images (jammy/noble/resolute x amd64/arm64) and `docker system prune`s
        # between them, so the default disk fills up.
        - task: Bash@3
          displayName: "Setup docker"
          inputs:
            targetType: "inline"
            script: |
              set -x
              sudo service docker stop
              sudo mkdir -p /etc/docker
              echo '{ "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json
              sudo service docker start

        # scripts/build_driver.py fetches playwright-core with `npm pack`, which
        # picks up the registry and credentials from this .npmrc.
        - task: Bash@3
          displayName: "setup .npmrc"
          inputs:
            targetType: "inline"
            script: echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc

        - task: npmAuthenticate@0
          displayName: "authenticate the private npm registry"
          inputs:
            workingFile: .npmrc

        - script: |
            python -m pip install --upgrade pip --disable-pip-version-check
            pip install -r local-requirements.txt --disable-pip-version-check
            pip install -r requirements.txt --disable-pip-version-check
            pip install -e . --disable-pip-version-check
          displayName: "Install dependencies"

        - task: AzureCLI@2
          displayName: "Login to ACR via OIDC"
          inputs:
            azureSubscription: "Playwright-CDN"
            scriptType: "bash"
            scriptLocation: "inlineScript"
            inlineScript: "az acr login --name playwright"

        - task: Bash@3
          displayName: "Register QEMU (binfmt) for arm64 cross-build"
          inputs:
            targetType: "inline"
            script: "docker run --rm --privileged ${ACR_CACHE_PREFIX}tonistiigi/binfmt --install arm64"
          env:
            ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/"

        - task: Bash@3
          displayName: "Build & publish Docker images"
          inputs:
            targetType: "inline"
            script: "./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }}"
          env:
            ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/"
            UBUNTU_MIRROR_PREFIX: "azure."

---

## File: .claude/skills/playwright-release/SKILL.md

---
name: playwright-release
---

# Preparing a Playwright Python release

In this skill, `1.xx` is the release (for example `1.64`), and `v1.xx.0` is its tag.
`$PW_SRC_DIR` is the roll checkout of `microsoft/playwright` (see the
`playwright-roll` skill, step 2).

Remotes: find the remote for `microsoft/playwright-python` with `git remote -v`.
The commands below call it `upstream`.

What you may push:
- The user's request to prepare the release is the instruction to open the two PRs
  in steps 2 and 3.
- Cut the release branch (step 4) only when the user tells you to.
- Never publish the release.

## 1. Preconditions

Check that the bugs are closed. The bug and regression issue templates give
these title prefixes:

```sh
gh issue list --repo microsoft/playwright-python --state open \
  --search 'in:title "[Bug]" OR in:title "[Regression]"'
```

If any are open, list them for the user and ask whether to continue. Do not
close or comment on issues.

Check that the driver is rolled. `DRIVER_VERSION` must be the driver for the
release: a `1.xx.y` build, or a `1.xx.0-beta-…` build from `release-1.xx`, as the
user says. If it is not, roll with the `playwright-roll` skill. This is often
already done. The roll commit goes into the release commit PR (step 3). When the
API roll for 1.xx is already on `main`, that PR only moves `DRIVER_VERSION`.

## 2. Release notes (microsoft/playwright)

Write the notes in the roll checkout, on a branch from `origin/main`:

```sh
git -C "$PW_SRC_DIR" fetch origin main
git -C "$PW_SRC_DIR" checkout -b release-notes-1.xx-python origin/main
```

In `docs/src/release-notes-python.md`, add a `## Version 1.xx` section above the
previous version. Translate it from the `## Version 1.xx` section of
`docs/src/release-notes-js.md`. Compare the `Version 1.63` sections of the two
files to see what a past translation dropped and changed.

- Keep the structure: highlights with emoji headings, `### New APIs` with
  `####` groups, `### Announcements`, `### Browser Versions`.
- Remove what Python does not have: the test runner (`@playwright/test`,
  `test.*`, config, fixtures, projects, reporters), UI mode, the HTML report,
  component testing, the VS Code extension, and `npx playwright test` options.
  Remove every API whose `langs` excludes Python. To check an API, look for it in
  `playwright/async_api/_generated.py`, or check `langs` in `api.json` (see
  "Verifying classifications" in the `playwright-roll` skill).
- Rewrite code samples in Python, with the sync API:
  `page.get_by_role("button").click()`. Use snake_case names and Python values
  (`True`, `None`).
- Keep API links as docs link macros: `` [`method: Page.frameLocator`] ``,
  `` [`option: Tracing.start.ariaSnapshots`] ``,
  `` [`event: Page.dialogClosed`] ``. The docs site shows them with Python names.
  In plain text, use Python names.
- Commands: write `playwright install`, not `npx playwright install`.
- Lean towards Python. List the playwright-python commits since the previous
  release branch:

  ```sh
  gh api repos/microsoft/playwright-python/compare/release-1.<xx-1>...main \
    --jq '.commits[].commit.message | split("\n")[0]'
  ```

  If some of them change what users see (for example, a different exception
  type), list them for the user and ask whether to add them to the notes.

Commit, push to the user's fork of `microsoft/playwright`, and open the PR. If
the checkout has no remote for the fork, add one. `gh api user --jq .login`
gives the user's login.

```sh
git -C "$PW_SRC_DIR" commit -am "docs: release notes for v1.xx Python"
git -C "$PW_SRC_DIR" push <fork-remote> release-notes-1.xx-python
gh pr create --repo microsoft/playwright --head <login>:release-notes-1.xx-python \
  --title "docs: release notes for v1.xx Python" \
  --body "- Add \`Version 1.xx\` to the Python release notes, translated from the Node.js notes"
```

Then put the roll checkout back on the release branch. `./scripts/update_api.sh`
reads the docs from it. Run the checkout checks from step 2 of the
`playwright-roll` skill.

```sh
git -C "$PW_SRC_DIR" checkout release-1.xx
```

## 3. Release commit (playwright-python)

Open a PR from the branch with the roll commit:

- Title: `chore: roll Playwright to v1.xx.y`, where `v1.xx.y` is the Python
  release version, even when `DRIVER_VERSION` is a beta build.
- Description: the release notes from step 2, in GitHub markdown (see below).
  The user copies this description into the GitHub release, so for this PR it
  replaces the "keep PR descriptions short" rule in `AGENTS.md`.

Convert the notes to GitHub markdown yourself:

- Leave out the `## Version 1.xx` heading. Move every other heading up one level
  (`###` becomes `##`, `####` becomes `###`).
- Replace each link macro with a full playwright.dev link. Use the Python name as
  the link text:

  | Macro | GitHub markdown |
  |---|---|
  | `` [`method: Page.frameLocator`] `` | `[page.frame_locator()](https://playwright.dev/python/docs/api/class-page#page-frame-locator)` |
  | `` [`method: Locator.visible`] `` (a property in Python) | `[locator.visible](https://playwright.dev/python/docs/api/class-locator#locator-visible)` |
  | `` [`option: Browser.newContext.httpCredentials`] `` | ``[`http_credentials`](https://playwright.dev/python/docs/api/class-browser#browser-new-context-option-http-credentials)`` |
  | `` [`event: BrowserContext.dialogClosed`] `` | `[browser_context.on("dialogclosed")](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-event-dialog-closed)` |

  The page is `class-` and the class name in lower case. The anchor is the class
  and member names in kebab-case. Options add `-option-<name>`, and events add
  `-event-<name>`. A member is a property in Python if `_generated.py` declares
  it with `@property`.

Wait for the user to review and merge the PR. Do not merge it yourself.

## 4. Release (only when the user tells you to)

Cut `release-1.xx` from `main` and push it to `upstream`:

```sh
git fetch upstream main
git show FETCH_HEAD:DRIVER_VERSION       # the release driver
git log -1 --oneline FETCH_HEAD          # the release commit from step 3
git ls-remote --exit-code upstream refs/heads/release-1.xx   # must fail: the branch must not exist yet
git push upstream FETCH_HEAD:refs/heads/release-1.xx
```

Save the release notes as a draft release that targets the branch. Take the
notes from the merged PR description, so that edits from the review are
included:

```sh
gh pr view <release-pr> --repo microsoft/playwright-python --json body --jq .body > <notes-file>
gh release create v1.xx.0 --repo microsoft/playwright-python --draft \
  --target release-1.xx --title v1.xx.0 --notes-file <notes-file>
gh release list --repo microsoft/playwright-python --limit 3   # v1.xx.0 shows as Draft
```

Give the user the URL of the draft. Your work ends here. Do not publish the
release: never run `gh release create` without `--draft`, and never run
`gh release edit --draft=false`. Publishing creates the tag and starts
`.github/workflows/publish.yml`, which builds the wheels and uploads them to PyPI
after a reviewer approves the `pypi` environment. Never run that workflow, and
never approve the environment.

---

## File: .claude/skills/playwright-roll/SKILL.md

---
name: playwright-roll
---

# Rolling Playwright Python

The goal of a roll is to move the driver pin in `DRIVER_VERSION` to a new release, port every public API change introduced upstream during that interval, and suppress the rest, so that `./scripts/update_api.sh` and `mypy playwright` run clean and the test suite still passes.

The previous human-facing summary lives in `../../../ROLLING.md`. This skill is the operational playbook — read it end to end before starting.

## Mental model

The Python port is hand-written code in `playwright/_impl/`, plus a generator (`scripts/codegen/generate_api.js`) that:

1. parses the upstream docs (`$PW_SRC_DIR/docs/src/api`) with the upstream parser and filters them for Python,
2. applies the Python facts that the docs do not contain, from `scripts/codegen/python_rules.js`, and
3. emits the whole public API into `playwright/{async,sync}_api/_generated.py`: signatures, types, docstrings, event overloads, and the call into `_impl/` for every member.

The generator never reads `_impl/`. A new member or option in the docs therefore shows up in `_generated.py` right away, as a call into `_impl/` that does not exist yet. Each generated class types its `_impl_obj`, so **`mypy playwright` lists every such gap** as an error in `_generated.py`. Three resolutions:

- **PORT** — the new API is intended for Python (no `langs.only` filter, or `langs.only` includes `"python"`). Implement it in `_impl/` until mypy is clean.
- **MISMATCH** — the shipped Python API differs from the docs (a callback signature uses unions, a kwarg uses a legacy name, etc.). The shipped implementation wins. Add an entry to `scripts/codegen/python_rules.js` with a comment explaining *why*, and propose the upstream docs change that would make the entry unnecessary.
- **N/A** — the commit only touches docs, has `* langs: js` (or any other filter that excludes Python), is server-side, Electron-only, or was reverted later in the same release. No action; the generator already leaves it out.

The upstream documentation source of truth is `docs/src/api/*.md` in the playwright repo. Every `## method:` / `## property:` / `## event:` / `### option:` / `### param:` block has an optional `* langs: js` (or `js, python`, etc.) filter. The Python doclint resolves these into `langs` fields on each member of `api.json`. **An empty `langs: {}` means "all languages including Python" — *implement it*, don't suppress it.**

> **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).

## Process

### 1. Set up the env

`CONTRIBUTING.md` covers this. Notes from past rolls:

- The repo requires Python 3.10+. If `python3.10` isn't available, use `python3` (3.12 is fine).
- If `python3-venv` is missing system-wide, use `uv venv env` instead, then `uv pip install --python env/bin/python --upgrade pip`. Don't try to `apt install` — sudo is denied in the harness.
- Always activate the venv before any `pip`, `pytest`, `mypy`, or `pre-commit` invocation.

### 2. Bump the driver pin, download it, and generate api.json

The docs walk and codegen read the upstream docs from a `microsoft/playwright`
checkout at `$PW_SRC_DIR`. **Use a separate checkout that exists only for
rolls, on the exact release branch you roll to**: `release-<new>`, the
`major.minor` of the new `DRIVER_VERSION` (the branch has the release's docs
plus any cherry-picks). Never point `PW_SRC_DIR` at a checkout that someone
works in: on `main` or a feature branch the docs differ, and the generator
silently emits that API instead of the release's.

```sh
export PW_SRC_DIR=../playwright-for-python      # the roll checkout, nothing else
# First roll on this machine: clone it (no npm install needed).
git clone https://github.com/microsoft/playwright.git "$PW_SRC_DIR"
# Every roll: move it to the release branch.
git -C "$PW_SRC_DIR" fetch origin
git -C "$PW_SRC_DIR" checkout release-<new>     # e.g. release-1.62
git -C "$PW_SRC_DIR" merge --ff-only origin/release-<new>
```

Check the checkout before you generate, and again before you commit:

```sh
git -C "$PW_SRC_DIR" rev-parse --abbrev-ref HEAD                  # must print release-<new>
git -C "$PW_SRC_DIR" status --porcelain                           # must print nothing
git -C "$PW_SRC_DIR" rev-parse HEAD origin/release-<new>          # both lines must be equal
```

Then bump the pins and assemble the driver:

```sh
# Edit DRIVER_VERSION (repo root): the playwright-core npm version for the new
# release, no "v" prefix, e.g.  1.62.0
python scripts/update_node_version.py     # refresh NODE_VERSION to the current LTS

source env/bin/activate
python -m build --wheel       # downloads playwright-core @ DRIVER_VERSION + Node.js, assembles the driver
playwright install chromium   # NOT --with-deps; sudo is denied

# The walk below inspects `langs` from api.json. Codegen does not need it.
node "$PW_SRC_DIR/utils/doclint/generateApiJson.js" > /tmp/api.json
```

The wheel build just downloads the `playwright-core` npm package at
`DRIVER_VERSION` and the matching Node.js binary (no source build, no Node/npm/git
toolchain), and unpacks the driver under `playwright/driver/`. The docs are the
one piece not shipped in the bundle — `./scripts/update_api.sh` reads them from
`$PW_SRC_DIR`. `/tmp/api.json` is only a convenient dump of the same docs for
the walk.

### 3. Identify the commit range

Use the roll checkout at `$PW_SRC_DIR` (from step 2).
Bring it up to date and ensure release branches/tags are present before walking
the range:

```sh
git -C "$PW_SRC_DIR" fetch --tags
git -C "$PW_SRC_DIR" fetch origin 'release-*:release-*'
```

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.

The diff range is "every commit on the new release branch since the previous release was cut". Anchor commits:

- **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.
  ```sh
  git -C "$PW_SRC_DIR" log --all --grep="bump version to v" --oneline | head
  ```
- **New release end**: the tip of `release-<new>` (or the matching tag if it exists).

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

```sh
git -C "$PW_SRC_DIR" log <prev-anchor>~1..release-<new> --oneline --reverse -- docs/src/api > /tmp/roll-<new>-commits.md
```

A normal roll yields 50–100 commits. If you see 0 or thousands, the range is wrong.

Format the file as a markdown checklist and add the standard preamble (status legend, where to look up `api.json` etc.) — see the file from the 1.58→1.59 roll for the template.

### 4. Walk the commit list

For each commit, in chronological order:

```sh
git -C "$PW_SRC_DIR" show <sha> -- docs/src/api/
```

Look for:
- `## (async )?method:` / `## property:` / `## event:` additions or removals;
- `* langs: ...` lines on those blocks;
- `### param:` / `### option:` additions or removals;
- new `class-X.md` files (whole new classes — usually `langs: js`);
- type changes in `- returns:` lines.

Classify and act.

#### Verifying classifications (do this before suppressing anything)

Before tagging anything as MISMATCH or N/A based on appearance, dump the actual `langs` from `api.json`:

```python
import json
data = json.load(open("/tmp/api.json"))
classes = {c["name"]: c for c in data}
for cls_name in ["Page", "BrowserContext", "Screencast", "Debugger"]:
    cls = classes.get(cls_name)
    if not cls:
        continue
    print(f"\n{cls_name}: cls_langs={cls.get('langs', {})}")
    for m in cls["members"]:
        print(f"  {m['name']} kind={m.get('kind')} langs={m.get('langs', {})}")
```

For options/params nested inside an `Object`-typed arg, walk one level deeper:

```python
for a in member.get("args", []):
    if a["name"] == "options":
        for prop in a.get("type", {}).get("properties", []):
            print(prop["name"], prop.get("langs", {}))
```

A few rules of thumb that catch most "actually a PORT" cases:
- If the *containing class* has empty `langs: {}` and the *member* has empty `langs: {}`, it's for Python — implement it.
- If the member is empty but a single *option* has `langs: js`, the method is for Python and you only skip that option (e.g. `Screencast.start.size` is `langs: js` while `Screencast.start` itself isn't).
- If you're about to add three or more `Method not implemented:` entries for the same class, stop — you almost certainly need to implement the class.

#### PORT

Implement the change in `playwright/_impl/<module>.py`. Use the upstream JS implementation as a reference: `$PW_SRC_DIR/packages/playwright-core/src/client/<module>.ts`. Translate idioms:

| Upstream JS | Python |
|---|---|
| `async foo(): Promise<X>` | `async def foo(self) -> X:` |
| `foo(): X` (sync getter, no args, no body) | `@property def foo(self) -> X:` (the generator makes argument-less sync members properties — see `isPythonProperty` in `scripts/codegen/generate_api.js` — and reads `self._impl_obj.foo` without calling it. mypy does not catch a plain method here; the test does.) |
| `await this._channel.foo({ a, b }, this._timeout(options))` | `await self._channel.foo(a=a, b=b, timeout=self._timeout(timeout))`. The generated channel method takes every protocol parameter, with no defaults; an absent option is `None`. The implementation method's own parameters use the Python (snake_case) names, with no defaults. |
| `(await this._channel.foo()).value` | `(await self._channel.foo())["value"]`. The result dict has every top-level field; a field that the driver left out is `None`. |
| `this._channel.on('foo', ({ a }) => ...)` | `self._channel.onFoo(lambda event: ...)`; `event["a"]` is `None` when the driver left the optional field out. |
| `try { ... } catch (e) { if (isTargetClosedError(e)) return; throw e; }` | `try: ...; except Exception as e: if is_target_closed_error(e): return; raise` (import from `playwright._impl._errors`) |
| Inline `[Object]` return like `{endpoint: string}` | A `TypedDict` in `playwright/_impl/_api_structures.py`, which the generator writes. It names the struct from the docs: `` "Name" <[Object]> ``, or an `* alias-python:` line under it. See `RemoteAddr`, `BrowserBindResult`, `DebuggerPausedDetails`. |
| `binary` event/return field | The Python channel layer hands you a base64 string. Decode with `base64.b64decode(value)` before exposing as `bytes`. See `Screencast._dispatch_frame`. |

When implementing a new ChannelOwner subclass (one constructed by the protocol with `(parent, type, guid, initializer)`):
1. Register it in `playwright/_impl/_object_factory.py:create_remote_object` — otherwise the guid resolves to `DummyObject` and downstream code breaks mysteriously.
2. Add it to `playwright/_impl/_api_classes.py` (the import and `__all__`). The generator emits every Python class in the docs, and the generated API imports each implementation from that file. If the class is missing there, `_generated.py` fails to import.

When implementing a non-ChannelOwner wrapper class (a plain class that holds a Page/Context reference, like `Screencast`, `Clock`):
- Add it to `playwright/_impl/_api_classes.py`, as for a ChannelOwner subclass.
- Set `self._loop = parent._loop` and `self._dispatcher_fiber = parent._dispatcher_fiber` in `__init__`. The generated `AsyncBase`/`SyncBase` wrappers read these; missing them gives `AttributeError: 'X' object has no attribute '_loop'` at first use.

When the docs add a new struct (an inline `[Object]` with named fields):
- The generator writes its TypedDict into `_api_structures.py`. Never edit that file by hand; internal types go in `_internal_structures.py`.
- It needs a Python name in the docs: a name before the type (`` - `size` "ScreenSize" <[Object]> ``), or an `* alias-python:` line under the type. The generator fails with "No Python name for the struct at …" until one exists. Nested structs need names too.
- Re-export it from both `playwright/async_api/__init__.py` and `playwright/sync_api/__init__.py`: assignment line plus an entry in `__all__`. Same pattern as `ViewportSize`, `RemoteAddr`.

If the new API was previously recorded in `python_rules.js` (for example in `extraMembers`), **remove that entry** when implementing it. The generator fails on entries that no longer match anything, so stale entries cannot pile up.

If a doc rename involves a *positional* parameter (no default, before any `*`), users almost certainly call it positionally — you can rename freely. The 1.59 `BrowserType.connect.wsEndpoint` → `endpoint` is the canonical example. Don't suppress this kind of rename; just rename in `_impl/`. **Important corollary:** when docs rename a param, the wire-protocol field usually also changed in `packages/protocol/spec/*.yml` and the server-side dispatcher in `packages/playwright-core/src/server/dispatchers/*Dispatcher.ts`. If so, you must also update the channel-send dict key (e.g. `{"wsEndpoint": …}` → `{"endpoint": …}`). Keeping the old name in Python for a renamed param hides a wire-protocol bug.

#### MISMATCH

A MISMATCH is a divergence between the docs and the shipped Python surface. The shipped implementation wins. Use it sparingly — most apparent mismatches turn out to be PORTs you skipped. Fix a type, a name or a position in the upstream docs: declare the param or option for Python only, with `* langs: python`, and limit the shared declaration to the other languages. Example from `class-page.md`, where Python accepts a callback with one or two arguments:

```md
### param: Page.route.handler
* since: v1.8
* langs: python
- `handler` <[function]\([Route]\)|[function]\([Route], [Request]\)>
```

`scripts/codegen/python_rules.js` only holds what the docs cannot say, for example hidden internal kwargs (`extraOptions`: `Browser.new_context(default_browser_type=)`). Add an entry to the right table, with a one-line `//` comment above it explaining *why*. Keys use docs names (`Class.member.arg`, camelCase).

The generator fails with "the entry … no longer matches anything; remove it" when an upstream change makes an entry unnecessary. Delete that entry.

**Do not suppress** these — they're PORTs in disguise:

- "Internal tooling" classes/methods whose `langs` field is empty (`Screencast.*`, `Debugger.*`, `Page.pick_locator`, `BrowserContext.debugger`, `Browser.bind/unbind`, `Page.{clear,console}_*`). The 1.59 roll suppressed all of these initially, then had to walk every one back. Verify `langs` first.
- A renamed positional parameter (mypy: unexpected keyword argument `newName` for the impl method). Just rename in `_impl/`; if the wire name changed too, mypy reports the generated channel call.
- A struct return the generator cannot name ("No Python name for the struct at …"). Name it, so that the generator writes a TypedDict — don't type it `Dict`.

#### N/A

Common N/A flavors:
- Whole new class with `langs: js` (Inspector/Screencast, Debugger, Overlay).
- Members with `langs: js` (most "tooling" / MCP / agentic features).
- Doc-only edits (typo fixes, "Improve `not` property sections", etc.).
- Reverts that cancel an earlier add in the same range (always check the rest of the range before porting something that gets reverted).
- Java/C# `langs:` blocks.
- Electron-only changes (`docs/src/api/class-electron.md`).

Tick the box in `/tmp/roll-<new>-commits.md` with one line: `[x] <sha> <subject> — <classification>: <one-liner>`.

### 5. Regenerate

```sh
./scripts/update_api.sh   # uses PW_SRC_DIR from step 2; run the checkout checks there first
```

The script, for `sync` and then `async`:
1. runs `node scripts/codegen/generate_api.js --flavor <flavor>` (reads the docs from `$PW_SRC_DIR`) straight into `playwright/<flavor>_api/_generated.py`,
2. invokes `pre-commit run --files` on the generated file.

It then runs `python scripts/codegen/generate_protocol.py`, which reads `packages/protocol/spec/*.yml` from `$PW_SRC_DIR` and writes the command parameter tables to `playwright/_impl/_protocol.py`, the message types to `playwright/_impl/_protocol_types.py` and the typed channels to `playwright/_impl/_protocol_channels.py`.

Then run `mypy playwright` and work through the errors in `_generated.py` — each one is a call into `_impl/` that the implementation does not support yet.

To see exactly what a regeneration changed in the public API, compare against the committed file:

```sh
git show HEAD:playwright/async_api/_generated.py > /tmp/old.py
python scripts/codegen/compare_api.py /tmp/old.py playwright/async_api/_generated.py --show all
```

**CI no longer verifies that `_generated.py` is in sync** (the Lint job dropped the
"Verify generated API is up to date" step so it needn't check out upstream). So
regenerating here and committing the result is on you — don't skip it.

Failure modes and fixes:

| Symptom | Cause | Fix |
|---|---|---|
| mypy: `"XImpl" has no attribute "y"` in `_generated.py` | The docs have a new member `X.y`; `_impl/` does not. | PORT it. |
| mypy: `Unexpected keyword argument "z" for "y" of "XImpl"` | New parameter or option on an existing method. | Add the parameter in `_impl/` under its Python (snake_case) name, as `Optional[X]` with no default, and pass it in the channel call. mypy then reports each caller that must pass it. If Python must not have it, fix the docs: `* langs:` on the option. |
| mypy: `Missing named argument "z" for "y" of "XChannel"` | The protocol added a parameter to a command. | Pass the value in the channel call. If the implementation method needs a new parameter for it, add it as above. |
| mypy: incompatible argument type in `_generated.py` | The docs type changed, or `_impl/` declares a narrower type. | Match the type in `_impl/`, or fix the docs with a Python-only declaration (`* langs: python`). |
| generator: `No Python name for the struct at X.y.z` | A new inline `[Object]` with no name in the docs. | Name it in the docs: `` "Name" <[Object]> ``. |
| generator: `The struct X has different fields at A and B` | Two places in the docs share one struct name, but their fields differ. | Give one place its own name in the docs. |
| `_generated.py` gains or loses members that the commit walk never mentioned | `PW_SRC_DIR` is not on `release-<new>` (for example on `main` or a feature branch), or is behind `origin/release-<new>`. | Run the checkout checks from step 2, fix the checkout, regenerate. |
| generator: `Unknown docs type "T" at X.y` | A docs type the type table in `generate_api.js` (`toPy`) does not know. | Add it to `toPy`. |
| generator: `required parameter z follows an optional one` | The docs order params so that Python would need a default before a non-default. | Fix the docs, for example with a Python-only `### param:` or `### option:`, as `select_option(value)` has. |
| generator: `the entry "…" no longer matches anything; remove it` | An upstream change made a rules entry unnecessary (or renamed what it points at). | Delete it, or re-key it to the new docs name. |
| `pyright … reportInconsistentOverload` (single-event class) | A class gained its first event, so there is one `Literal[…]` overload + impl, which pyright wants ≥2 of. | The generator already handles this — `emitEvents` adds a second `@typing.overload` with `event: str` when a class has exactly one event. |
| `pre-commit` keeps reformatting `_generated.py` on each run | First run after regen always reformats once; rerun until idle. | `pre-commit run --all-files` to settle. |
| A kwarg in `_impl/` that the docs don't mention (e.g. legacy name from a doc rename) | Nothing fails: the generator simply never passes it. | If the param is positional with no default, just rename it in `_impl/`. Check `packages/protocol/spec/*.yml` and the server dispatcher — if the wire field renamed too, also update the channel-send dict key. Only use `extraOptions` for genuinely hidden internal kwargs (`default_browser_type`). |
| `AttributeError: 'X' object has no attribute '_loop'` (or `_dispatcher_fiber`) at first use of a new wrapper class | The non-ChannelOwner wrapper isn't initializing the fields the generated `AsyncBase`/`SyncBase` reads. | In the wrapper's `__init__`, set `self._loop = parent._loop` and `self._dispatcher_fiber = parent._dispatcher_fiber`. |
| `'NoneType' object has no attribute 'get'` after `send_return_as_dict` | Method's protocol response carries no fields and `send_return_as_dict` returned `None`. | `(result or {}).get(...)`. |
| A test fails at teardown with `Protocol violations: X.y: params.z: unknown` (or `missing`, or `expected …`) | The client sends a message that the protocol spec does not allow. The driver would drop an unknown parameter without an error. | Fix the send in `_impl/`: rename the key after a wire rename, or remove a client-side option before the send. |
| Frame/buffer payload arrives as a `str` instead of `bytes` | Protocol `binary` fields cross the wire as base64. | `base64.b64decode(value)` in the impl before exposing. |

After the script settles, run `pre-commit run --all-files` once more to confirm everything is idle.

### 6. Add tests

For each PORT, port the upstream test that covers it, as an async test and a matching sync test. Conventions:

- The upstream file decides the Python file: `tests/page/expect-misc.spec.ts` becomes `tests/async/test_expect_misc.py` and `tests/sync/test_expect_misc.py`. Create the file if it does not exist.
- The upstream title decides the test name: the `describe` title and the test title, in snake_case. `test('should support pseudo element')` in `test.describe('toHaveCSS')` becomes `test_to_have_css_should_support_pseudo_element`.
- When no upstream test covers the change, extend an existing test before you add a new one.
- Use `from playwright.async_api import …`, **not** `from playwright._impl._page import Page` (the impl class doesn't have the public wrappers like `expect_console_message`).
- For event-info objects, `await message_info.value` (it's an `async` property).
- Don't write tests that hang the page (e.g. `page.evaluate(... fetch slow ...)` followed by `page.close()` from a fixture) — the request task gets a `TargetClosedError`. Use `page.on("event", handler)` to capture state at event time instead.
- `playwright install chromium` (no `--with-deps`) is sufficient for the test suite under sandbox.

### 7. Update existing high-touch artifacts

- `DRIVER_VERSION` and `NODE_VERSION`: already done in step 2.
- `README.md`: gets the chromium/firefox/webkit version table updated automatically by `scripts/update_versions.py` (called from `update_api.sh`). Don't edit by hand.
- The "Backport changes" tracking issue on GitHub (filed by `microsoft-playwright-automation`) is the *intent* tracker, but it's frequently out of sync with what's actually been ported. Treat it as a starting point, not the source of truth — the `docs/src/api/` commit walk is authoritative.

### 8. Final verification

Repeat the checkout checks from step 2: the committed `_generated.py` must come from `release-<new>`.

```sh
pre-commit run --all-files
mypy playwright                 # 2 pre-existing errors in _json_pipe.py and _artifact.py are unrelated
pytest --browser chromium tests/async/<files-you-touched> tests/sync/<files-you-touched>
```

Then a smoke regression on a few neighboring suites (`tests/async/test_browser*.py`, `test_cdp_session.py`, `test_tracing.py`, `test_dialog.py`, `test_page_*.py`) to make sure nothing inherent to the port shifted.

## Reference: `python_rules.js` tables

All keys use docs names: `Class.member` or `Class.member.arg`, with the upstream PascalCase class and camelCase member/arg names. `*` matches one name segment.

| Table | Use |
|---|---|
| `handWrittenClasses` | Classes in the Python docs that are written by hand (`Error`, `TimeoutError`, `FormData`). |
| `contextManagerClasses`, `tracebackHideClasses` | Base-class and `__tracebackhide__` choices. |
| `extraMembers`, `extraOptions` | Python-only members and hidden options; generated without docstrings. |
| `positionalOptions` | Option names that are positional everywhere (`predicate`). |
| `structTotal` | `total=` of a TypedDict, where it differs from "total=False if any field is optional". |

## Tips & gotchas

- **`langs.only` is your filter — and the only filter.** Don't classify by name (`Screencast`, `Debugger`, `pickLocator`) or by intuition ("looks like internal tooling"). Always check `langs` in `api.json`. The 1.59 roll cost two extra audit passes by trusting names over `langs`.
- **Audit your own classifications a second time.** After the first walk through the commit range, before opening the PR, re-read every entry in `python_rules.js` you added during this roll and ask "is this divergence justified, or did I skip a port?" Run the `langs`-dump snippet on each suspicious entry. The 1.59 roll's first PR had ~20 wrong suppressions; the second pass cut them to 0.
- **A cluster of rules on the same class is a smell.** If you're about to add five entries for `Foo.*`, you're almost certainly looking at a class that needs to be implemented. Implement the whole thing once and the entries disappear.
- **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.
- **Watch for rename-revert pairs.** 1.59 had `Locator.normalize` → `Locator.toCode` (#39648) → `Locator.normalize` (#39754). Final state wins; only port the last.
- **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 "$PW_SRC_DIR" show <sha> -- packages/protocol/spec/` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the regenerated `_protocol_channels.py` renames the argument of the channel method, and mypy reports each call in `_impl/` that passes the old name.
- **TypedDicts beat `Dict[str, X]` for any structured return.** When the docs describe a return as `[Object]` with named fields, name the struct for the generator, which writes its `TypedDict` into `_api_structures.py`, and re-export it from both public `__init__.py` files. Zero runtime cost (it's still a `dict`), and the docstrings expand its fields from the docs.
- **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.
- **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.
- **The docs parser has quirks.** In a type expression, everything after a function's `:` is its return type, so `[function]([A]):[any]|[B]` is one function returning `any|B`. Put a function with a return type last in a union. And when a `### param:` header name differs from the backticked name on its type line, the header name is the docs name and the type-line name becomes the alias.
- **Don't edit `_generated.py` to fix lint or typing.** Fix `_impl/`, `scripts/codegen/generate_api.js`, or `python_rules.js` instead. Hand-editing the generated file is reverted on the next regen.
- **`/tmp/roll-<new>-commits.md` is a working artifact, not a deliverable.** Don't commit it. The commit message and PR description are where the audit summary belongs.

---

## File: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"
    cooldown:
      default-days: 7

---

## File: .github/ISSUE_TEMPLATE/bug.yml

name: Bug Report 🪲
title: '[Bug]: '
body:
  - type: markdown
    attributes:
      value: |
        # Please follow these steps first:
  - type: markdown
    attributes:
      value: |
        ## Troubleshoot
        If Playwright is not behaving the way you expect, we'd ask you to look at the [documentation](https://playwright.dev/python/docs/intro) and search the issue tracker for evidence supporting your expectation.
        Please make reasonable efforts to troubleshoot and rule out issues with your code, the configuration, or any 3rd party libraries you might be using.
        Playwright offers [several debugging tools](https://playwright.dev/python/docs/debug) that you can use to troubleshoot your issues.
  - type: markdown
    attributes:
      value: |
        ## Ask for help through appropriate channels
        If you feel unsure about the cause of the problem, consider asking for help on for example [StackOverflow](https://stackoverflow.com/questions/ask) or our [Discord channel](https://aka.ms/playwright/discord) before posting a bug report. The issue tracker is not a help forum.
  - type: markdown
    attributes:
      value: |
        ## Make a minimal reproduction
        To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
        The simpler you can make it, the more likely we are to successfully verify and fix the bug.
  - type: markdown
    attributes:
      value: |
        > [!IMPORTANT]
        > Bug reports without a minimal reproduction will be rejected.

        ---
  - type: input
    id: version
    attributes:
      label: Version
        The version of Playwright you are using.
        Is it the [latest](https://github.com/microsoft/playwright-python/releases)? Test and see if the bug has already been fixed.
      placeholder: ex. 1.41.1
    validations:
      required: true
  - type: textarea
    id: reproduction
    attributes:
      label: Steps to reproduce
      value: |
        Example steps (replace with your own):
        1. Clone my repo at https://github.com/<myuser>/example
        2. pip install -r requirements.txt
        3. python test.py
        4. You should see the error come up
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: Expected behavior
      placeholder: I expect to see X or Y
    validations:
      required: true
  - type: textarea
    id: what-happened
    attributes:
      label: Actual behavior
        A clear and concise description of the unexpected behavior.
        Please include any relevant output here, especially any error messages.
      placeholder: A bug happened!
    validations:
      required: true
  - type: textarea
    id: context
    attributes:
      label: Additional context
    validations:
      required: false
  - type: textarea
    id: envinfo
    attributes:
      label: Environment
        Please provide information about the environment you are running in.
      value: |
        - Operating System: [Ubuntu 22.04]
        - CPU: [arm64]
        - Browser: [All, Chromium, Firefox, WebKit]
        - Python Version: [3.12]
        - Other info:
      render: Text
    validations:
      required: true

---

## File: .github/ISSUE_TEMPLATE/config.yml

blank_issues_enabled: false
contact_links:
  - name: Join our Discord Server
    url: https://aka.ms/playwright/discord
    about: Ask questions and discuss with other community members

---

## File: .github/ISSUE_TEMPLATE/documentation.yml

name: Documentation 📖
title: '[Docs]: '
labels: ['Documentation :book:']
body:
  - type: markdown
    attributes:
      value: |
        ### Thank you for helping us improve our documentation!
        Please be sure you are looking at [the Next version of the documentation](https://playwright.dev/python/docs/next/intro) before opening an issue here.
  - type: textarea
    id: links
    attributes:
      label: Page(s)
        Links to one or more documentation pages that should be modified.
        If you are reporting an issue with a specific section of a page, try to link directly to the nearest anchor.
        If you are suggesting that a new page be created, link to the parent of the proposed page.
    validations:
      required: true
  - type: textarea
    id: description
    attributes:
      label: Description
        Describe the change you are requesting.
        If the issue pertains to a single function or matcher, be sure to specify the entire call signature.
    validations:
      required: true

---

## File: .github/ISSUE_TEMPLATE/feature.yml

name: Feature Request 🚀
title: '[Feature]: '
body:
  - type: markdown
    attributes:
      value: |
        ### Thank you for taking the time to suggest a new feature!
  - type: textarea
    id: description
    attributes:
      label: '🚀 Feature Request'
    validations:
      required: true
  - type: textarea
    id: example
    attributes:
      label: Example
    validations:
      required: false
  - type: textarea
    id: motivation
    attributes:
      label: Motivation
        Outline your motivation for the proposal. How will it make Playwright better?
    validations:
      required: true

---

## File: .github/ISSUE_TEMPLATE/question.yml

name: 'Questions / Help 💬'
title: '[Please read the message below]'
labels: [':speech_balloon: Question']
body:
  - type: markdown
    attributes:
      value: |
        ## Questions and Help 💬

        This issue tracker is reserved for bug reports and feature requests.

        For anything else, such as questions or getting help, please see:

        - [The Playwright documentation](https://playwright.dev)
        - [Our Discord server](https://aka.ms/playwright/discord)
  - type: checkboxes
    id: no-post
    attributes:
      label: |
        Please do not submit this issue.
        > [!IMPORTANT]
        > This issue will be closed.
      options:
        - label: I understand
          required: true

---

## File: .github/ISSUE_TEMPLATE/regression.yml

name: Report regression
title: "[Regression]: "

body:
  - type: markdown
    attributes:
      value: |
        # Please follow these steps first:
  - type: markdown
    attributes:
      value: |
        ## Make a minimal reproduction
        To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the regression.
        The simpler you can make it, the more likely we are to successfully verify and fix the regression.
  - type: markdown
    attributes:
      value: |
        > [!IMPORTANT]
        > Regression reports without a minimal reproduction will be rejected.

        ---
  - type: input
    id: goodVersion
    attributes:
      label: Last Good Version
        Last version of Playwright where the feature was working.
      placeholder: ex. 1.40.1
    validations:
      required: true
  - type: input
    id: badVersion
    attributes:
      label: First Bad Version
        First version of Playwright where the feature was broken.
        Is it the [latest](https://github.com/microsoft/playwright-python/releases)? Test and see if the regression has already been fixed.
      placeholder: ex. 1.41.1
    validations:
      required: true
  - type: textarea
    id: reproduction
    attributes:
      label: Steps to reproduce
      value: |
        Example steps (replace with your own):
        1. Clone my repo at https://github.com/<myuser>/example
        2. pip -r requirements.txt
        3. python test.py
        4. You should see the error come up
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: Expected behavior
      placeholder: I expect to see X or Y
    validations:
      required: true
  - type: textarea
    id: what-happened
    attributes:
      label: Actual behavior
      placeholder: A bug happened!
    validations:
      required: true
  - type: textarea
    id: context
    attributes:
      label: Additional context
    validations:
      required: false
  - type: textarea
    id: envinfo
    attributes:
      label: Environment
        Please provide information about the environment you are running in.
      value: |
        - Operating System: [Ubuntu 22.04]
        - CPU: [arm64]
        - Browser: [All, Chromium, Firefox, WebKit]
        - Python Version: [3.12]
        - Other info:
      render: Text
    validations:
      required: true

---

## File: .github/workflows/ci.yml

name: CI

on:
  push:
    branches:
      - main
      - release-*
  pull_request:
    branches:
      - main
      - release-*

concurrency:
  # For pull requests, cancel all currently-running jobs for this workflow
  # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  infra:
    name: Lint
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: "3.10"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r local-requirements.txt
        pip install -r requirements.txt
        pip install -e .
    - name: Lint
      run: pre-commit run --show-diff-on-failure --color=always --all-files

  build:
    name: Build
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.14']
        browser: [chromium, firefox, webkit]
        exclude:
        # WebKit on standard macOS-latest (currently macos-15-arm64) is unstable;
        # upstream pins paid macos-15-xlarge for cross-browser webkit too.
        - os: macos-latest
          browser: webkit
        include:
        - os: windows-latest
          python-version: '3.10'
          browser: chromium
        - os: macos-latest
          python-version: '3.10'
          browser: chromium
        - os: ubuntu-latest
          python-version: '3.10'
          browser: chromium
        - os: macos-15-xlarge
          python-version: '3.14'
          browser: webkit
        - os: windows-latest
          python-version: '3.11'
          browser: chromium
        - os: macos-latest
          python-version: '3.11'
          browser: chromium
        - os: ubuntu-latest
          python-version: '3.11'
          browser: chromium
        - os: windows-latest
          python-version: '3.12'
          browser: chromium
        - os: macos-latest
          python-version: '3.12'
          browser: chromium
        - os: ubuntu-latest
          python-version: '3.12'
          browser: chromium
        - os: windows-latest
          python-version: '3.13'
          browser: chromium
        - os: macos-latest
          python-version: '3.13'
          browser: chromium
        - os: ubuntu-latest
          python-version: '3.13'
          browser: chromium
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies & browsers
      run: |
        python -m pip install --upgrade pip
        pip install -r local-requirements.txt
        pip install -r requirements.txt
        pip install -e .
        python -m build --wheel
        python -m playwright install --with-deps ${{ matrix.browser }}
    - name: Common Tests
      run: pytest tests/common --browser=${{ matrix.browser }} --timeout 90
    - name: Test Reference count
      run: pytest tests/test_reference_count_async.py --browser=${{ matrix.browser }}
    - name: Test Wheel Installation
      run: pytest tests/test_installation.py --browser=${{ matrix.browser }}
    - name: Test Sync API
      if: matrix.os != 'ubuntu-latest'
      run: pytest tests/sync --browser=${{ matrix.browser }} --timeout 90
    - name: Test Sync API
      if: matrix.os == 'ubuntu-latest'
      run: xvfb-run pytest tests/sync --browser=${{ matrix.browser }} --timeout 90
    - name: Test Async API
      if: matrix.os != 'ubuntu-latest'
      run: pytest tests/async --browser=${{ matrix.browser }} --timeout 90
    - name: Test Async API
      if: matrix.os == 'ubuntu-latest'
      run: xvfb-run pytest tests/async --browser=${{ matrix.browser }} --timeout 90

  test-stable:
    name: Stable
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        browser-channel: [chrome]
        include:
        - os: windows-latest
          browser-channel: msedge
        - os: macos-latest
          browser-channel: msedge
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: "3.10"
    - name: Install dependencies & browsers
      run: |
        python -m pip install --upgrade pip
        pip install -r local-requirements.txt
        pip install -r requirements.txt
        pip install -e .
        python -m build --wheel
        python -m playwright install ${{ matrix.browser-channel }} --with-deps
    - name: Common Tests
      run: pytest tests/common --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90
    - name: Test Sync API
      if: matrix.os != 'ubuntu-latest'
      run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90
    - name: Test Sync API
      if: matrix.os == 'ubuntu-latest'
      run: xvfb-run pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90
    - name: Test Async API
      if: matrix.os != 'ubuntu-latest'
      run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90
    - name: Test Async API
      if: matrix.os == 'ubuntu-latest'
      run: xvfb-run pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90

  test_examples:
    name: Examples
    runs-on: ubuntu-22.04
    defaults:
      run:
        working-directory: examples/todomvc/
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: '3.10'
    - name: Install dependencies & browsers
      run: |
        pip install -r requirements.txt
        python -m playwright install --with-deps chromium
    - name: Common Tests
      run: pytest

---

## File: .github/workflows/publish.yml

name: Publish

# Builds the wheels of a release tag and uploads them to PyPI. Uses PyPI trusted
# publishing: PyPI trusts this workflow file in the "pypi" environment, so no
# PyPI token is stored. Runs when a release is published. To publish an existing
# tag, run it by hand with the tag name.
on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      tag:
        required: true

permissions:
  contents: read

jobs:
  build:
    name: Build wheels
    runs-on: ubuntu-latest
    env:
      TAG: ${{ inputs.tag || github.event.release.tag_name }}
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        ref: refs/tags/${{ env.TAG }}
        # setuptools-scm reads the version from the tag.
        fetch-depth: 0
    - name: Check the tag
      run: |
        if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
          echo "Can only publish a release tag (vX.Y.Z), got: $TAG"
          exit 1
        fi
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: "3.10"
    - name: Set up Node.js
      uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
      with:
        node-version: '24'
    - name: Install dependencies
      run: python -m pip install "$(grep '^build==' local-requirements.txt)"
    - name: Build wheels
      run: |
        for wheel in $(python setup.py --list-wheels); do
          PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir dist
        done
    - name: Check the wheels
      run: |
        ls -l dist
        test "$(ls dist/*.whl | wc -l)" -eq "$(python setup.py --list-wheels | wc -l)"
        for file in dist/*; do
          if [[ "$(basename "$file")" != "playwright-${TAG#v}-"*.whl ]]; then
            echo "Unexpected file for $TAG: $file"
            exit 1
          fi
        done
    - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
      with:
        name: wheels
        path: dist/
        if-no-files-found: error

  publish:
    name: Upload to PyPI
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/project/playwright/
    permissions:
      # Lets the job get the GitHub OIDC token that PyPI trusted publishing needs.
      id-token: write
    steps:
    - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
      with:
        name: wheels
        path: dist/
    - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

---

## File: .github/workflows/test_docker.yml

name: Test Docker
on:
  push:
    paths:
      - '.github/workflows/test_docker.yml'
      - 'setup.py'
      - '**/Dockerfile.*'
    branches:
      - main
      - release-*
  pull_request:
    paths:
      - '.github/workflows/test_docker.yml'
      - 'setup.py'
      - '**/Dockerfile.*'
    branches:
      - main
      - release-*
jobs:
  build:
    timeout-minutes: 120
    runs-on: ${{ matrix.runs-on }}
    strategy:
      fail-fast: false
      matrix:
        docker-image-variant:
          - jammy
          - noble
          - resolute
        runs-on:
          - ubuntu-24.04
          - ubuntu-24.04-arm
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    - name: Set up Python
      uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
      with:
        python-version: "3.10"
    - name: Set up Node.js
      uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
      with:
        node-version: '24'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r local-requirements.txt
        pip install -r requirements.txt
        pip install -e .
    - name: Build Docker image
      run: |
        ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}"
        bash utils/docker/build.sh --$ARCH ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }}
    - name: Test
      run: |
        CONTAINER_ID="$(docker run --rm -e CI -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)"
        # Fix permissions for Git inside the container
        docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright
        docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt
        docker exec "${CONTAINER_ID}" pip install -r requirements.txt
        docker exec "${CONTAINER_ID}" pip install -e .
        # build.sh (above) already built the driver bundles into driver/ on the
        # host. The repo is bind-mounted into the container, so this in-container
        # wheel build reuses those bundles (setup.py skips the source build when
        # the zip already exists) and therefore needs no Node.js in the image.
        docker exec "${CONTAINER_ID}" python -m build --wheel
        docker exec "${CONTAINER_ID}" xvfb-run pytest tests/sync/
        docker exec "${CONTAINER_ID}" xvfb-run pytest tests/async/

## 2. Official Technical Reference & Guides (microsoft/microsoft.github.io)

## File: README.md

[microsoft.github.io](https://microsoft.github.io)
===================

Many open source projects released by Microsoft products, services and teams also have
deployments powered by GitHub Pages on this domain.

To discover Microsoft-released open source projects, learn about Microsoft's open source
approach and other information, check us out online at [opensource.microsoft.com](https://opensource.microsoft.com).

If we can help answer questions about the open source program at Microsoft, you can
contact `opensource at microsoft com` - although we are not able to provide support
for projects through this mechanism.

## Microsoft GitHub resources

- Default repo templates including `SECURITY.MD` and code of conduct files: [microsoft/repo-templates](https://github.com/microsoft/repo-templates)
- [Code of Conduct](https://opensource.microsoft.com/codeofconduct) ([FAQ](https://opensource.microsoft.com/codeofconduct/faq/))

## Microsoft open source resources

- [Open Source Blog](https://cloudblogs.microsoft.com/opensource/)
- [The Open Source Show on Channel 9](https://channel9.msdn.com/Shows/The-Open-Source-Show)
- [https://opensource.microsoft.com/](https://opensource.microsoft.com/)

## Resources for Microsoft employees

- [docs.opensource.microsoft.com](https://docs.opensource.microsoft.com)
- [Link your GitHub Account and join GitHub organizations automatically](https://repos.opensource.microsoft.com)

## How to identify an official Microsoft organization on GitHub

- Official Microsoft GitHub orgs have a professional avatar and include a web address at Microsoft.com or an associated official domain
- Official Microsoft orgs are [Verified on GitHub](https://github.blog/changelog/2018-08-07-domain-verification/) - look for the domain verification indicator on GitHub

---

## File: SECURITY.MD

# IMPORTANT: THIS IS NOT THE SOURCE OF TRUTH FOR THIS FILE

The `SECURITY.MD` file used by default in new Microsoft GitHub repos can be found
in the repo `microsoft/repo-templates` at https://github.com/microsoft/repo-templates.

Please contribute any fixes there.

------





## Security

Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).

If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.

## Reporting Security Issues

**Please do not report security vulnerabilities through public GitHub issues.**

Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).

If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).

You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 

Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:

  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
  * Full paths of source file(s) related to the manifestation of the issue
  * The location of the affected source code (tag/branch/commit or direct URL)
  * Any special configuration required to reproduce the issue
  * Step-by-step instructions to reproduce the issue
  * Proof-of-concept or exploit code (if possible)
  * Impact of the issue, including how an attacker might exploit the issue

This information will help us triage your report more quickly.

If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.

## Preferred Languages

We prefer all communications to be in English.

## Policy

Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).

---

## File: codeofconduct/index.md

# Microsoft Open Source Code of Conduct

This code of conduct outlines expectations for participation in Microsoft-managed open source communities, as well as steps for reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all. People violating this code of conduct may be banned from the community.

Our open source communities strive to:

* **Be friendly and patient:** Remember you might not be communicating in someone else's primary spoken or programming language, and others may not have your level of understanding.
* **Be welcoming:** Our communities welcome and support people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
* **Be respectful:** We are a world-wide community of professionals, and we conduct ourselves professionally. Disagreement is no excuse for poor behavior and poor manners. Disrespectful and unacceptable behavior includes, but is not limited to:
    * Violent threats or language.
    * Discriminatory or derogatory jokes and language.
    * Posting sexually explicit or violent material.
    * Posting, or threatening to post, people's personally identifying information ("doxing").
    * Insults, especially those using discriminatory terms or slurs.
    * Behavior that could be perceived as sexual attention.
    * Advocating for or encouraging any of the above behaviors.
* **Understand disagreements:** Disagreements, both social and technical, are useful learning opportunities. Seek to understand the other viewpoints and resolve differences constructively.
* This code is not exhaustive or complete. It serves to capture our common understanding of a productive, collaborative environment. We expect the code to be followed in spirit as much as in the letter.

## Scope
This code of conduct applies to all repos and communities for Microsoft-managed open source projects regardless of whether or not the repo explicitly calls out its use of this code. The code also applies in public spaces when an individual is representing a project or its community. Examples include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

Note: Some Microsoft-managed communities have codes of conduct that pre-date this document and issue resolution process. While communities are not required to change their code, they are expected to use the resolution process outlined here.  The review team will coordinate with the communities involved to address your concerns.

## Reporting Code of Conduct Issues
We encourage all communities to resolve issues on their own whenever possible. This builds a broader and deeper understanding and ultimately a healthier interaction. In the event that an issue cannot be resolved locally, please feel free to report your concerns by contacting [opencode@microsoft.com](mailto:opencode@microsoft.com).  Your report will be handled in accordance with the issue resolution process described in the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/).

In your report please include:

* Your contact information.
* Names (real, usernames or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well.
* Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment.
* Any additional information that may be helpful.

All reports will be reviewed by a multi-person team and will result in a response that is deemed necessary and appropriate to the circumstances. Where additional perspectives are needed, the team may seek insight from others with relevant expertise or experience. The confidentiality of the person reporting the incident will be kept at all times. Involved parties are never part of the review team.

Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the review team may take any action they deem appropriate, including a permanent ban from the community.

_This code of conduct is based on the template established by the [TODO Group](http://todogroup.org/) and used by numerous other large communities (e.g., [Facebook](https://code.facebook.com/pages/876921332402685/open-source-code-of-conduct), Twitter, GitHub and the Scope section from the [Contributor Covenant version 1.4](http://contributor-covenant.org/version/1/4/)._

---

## File: codeofconduct/faq/index.md

# Microsoft Open Source Code of Conduct FAQ

## Contents

* [Why have a Code of Conduct?](#whycode)
* [Why this Code of Conduct?](#whythis)
* [Why do this across all repos?](#allrepos)
* [How does a project adopt the code and process?](#howadopt)
* [What if my Microsoft project already has a Code of Conduct?](#alreadyhave)
* [How do I raise a concern?](#howreport)
* [What is the process for addressing issues that arise?](#process)
* [How does the resolution team work?](#howteamworks)
* [Can I use this Code of Conduct in my projects?](#caniuse)

## <a name="whycode"></a>Why have a Code of Conduct?
People and communities are the foundation of open source. Communities thrive through diversity of thought and the safety of its members. Microsoft has a deep-seated commitment to diversity and inclusion. All Microsoft employees enjoy a safe work environment and a culture of mutual respect and responsibility. Our team members and open source partners should enjoy the same environment when collaborating on open source projects.

While conduct issues seldom arise, when they do, they are often very public and very passionate. It is best for the community to be prepared with:

* A clear and accessible **Code of Conduct** stating the norms under which projects operate
* An **Issue Resolution Process** for addressing cases where these norms are not being maintained

## <a name="whythis"></a>Why this Code of Conduct?
The [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) is an instantiation of the [TODO Group](http://todogroup.org/) Code of Conduct template, that captures the Microsoft culture of equality, respect and inclusion. This same template is used by industry colleagues such as Facebook, Twitter, GitHub, Yahoo and others in their open source projects.

## <a name="allrepos"></a>Why do this across all repos?
Having one code and process is both efficient and consistent &mdash; community members can participate in any Microsoft-driven project and be confident that they know the norms and that they are supported by a robust process.

## <a name="howadopt"></a>How does a project adopt the code and process?
All Microsoft projects are automatically covered by the Code of Conduct and the Issue Resolution Process. **However**, it is critical that everyone in the communities be aware of the code and process. For that reason, all projects must link to the Code of Conduct in their README and/or CONTRIBUTING files using the following markdown.

```
This project has adopted the [Microsoft Open Source Code of
Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct
FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com)
with any additional questions or comments.
```

## <a name="alreadyhave"></a>What if my Microsoft project already has a Code of Conduct?
Thanks for being proactive in this important area. You are welcome to continue using your existing Code of Conduct, or you may change to the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Either way, all Microsoft projects must use the same issue resolution process. Please ensure that your documentation and guidance is updated to point people at the process outlined in this FAQ (see below).

Projects in the .NET Foundation are already covered by the [.NET Foundation Contributors Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). As such, there is no need for .NET Foundation projects to make any changes.

## <a name="howreport"></a>How do I raise a concern?
If you have witnessed or been subjected to a violation of the Code of Conduct, please send an email to [opencode@microsoft.com](mailto:opencode@microsoft.com). Your message will be handled in a secure and confidential manner. This email address is monitored by people who are not active in open source projects or communities &mdash; so you can be sure that you are not communicating with a person involved in the issue you are reporting. You will receive a response within one business day acknowledging receipt of your email and describing the process for its resolution. (See below for more details.)

## <a name="process"></a>What is the process for addressing issues that arise?
Emails sent to [opencode@microsoft.com](mailto:opencode@microsoft.com) kick off the following process:

* Your message will be acknowledged within one business day.
* Within the following business day, a small, 2-3, person team will be assembled from a pool of Microsoft employee volunteers to review your concern. This team will be as diverse as possible given its size and will pull in additional people as needed to gain further insight and provide guidance. The team will not include anyone directly involved in the issue that has been raised.
* From there team will work with you and the others involved to come to a conclusion. While issue complexity varies, the goal is to resolve issues within five working days.
* All communication will be confidential with very limited circulation.



## <a name="howteamworks"></a>How does the resolution team work?
Beyond the norms and values set out in the Code of Conduct, issue review teams operate under the following principles:

* **Less is more** &mdash; As much as possible, let the community work it out. It is much better to have communities self-correct than to have outsiders come in and “fix” problems.
* **Equality** &mdash; Contribution value or status in the community are not relevant to the review. Key people do not have more rights (either to abuse or be protected from abuse) than others in the community.
* **Independence** &mdash; Reviewers must act and be seen to act with independence from the project(s) in question and from Microsoft.

## <a name="caniuse"></a>Can I use this Code of Conduct in my projects?
Yes. The document is licenced under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0). You will of course need to update the document to suit the specifics of your projects, but by all means use this however you can to drive great communities.