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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/playwright-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Open a PR from the branch with the roll commit:
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 `CLAUDE.md`.
replaces the "keep PR descriptions short" rule in `AGENTS.md`.

Convert the notes to GitHub markdown yourself:

Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@ After the script settles, run `pre-commit run --all-files` once more to confirm

### 6. Add tests

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

- Tests go in the existing topic file (`test_page_network_request.py`, `test_browsercontext.py`, `test_dialog.py`, …) — don't create new files unless there's no obvious home.
- 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.
Expand Down
23 changes: 20 additions & 3 deletions CLAUDE.md → AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLAUDE.md
# AGENTS.md

Guidance for Claude when working in this repository.
Guidance for coding agents working in this repository.

## What this is

Expand All @@ -14,7 +14,7 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
- `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` — **auto-generated** by `scripts/codegen/generate_protocol.py` (run by `./scripts/update_api.sh`) from the upstream protocol spec: 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. Most new tests are added to the async file with a sync mirror.
- `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).
Expand Down Expand Up @@ -45,6 +45,23 @@ If the system lacks `python3-venv`, `uv venv env` is an acceptable substitute (t

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:
Expand Down
3 changes: 2 additions & 1 deletion playwright/_impl/_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,10 @@ async def not_to_have_css(
name: str,
value: Union[str, Pattern[str]],
timeout: float = None,
pseudo: Literal["after", "before"] = None,
) -> None:
__tracebackhide__ = True
await self._not.to_have_css(name, value, timeout)
await self._not.to_have_css(name, value, timeout, pseudo)

async def to_have_id(
self,
Expand Down
5 changes: 4 additions & 1 deletion playwright/async_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14112,6 +14112,7 @@ async def not_to_have_css(
name: str,
value: typing.Union[str, typing.Pattern[str]],
*,
pseudo: typing.Optional[Literal["after", "before"]] = None,
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
) -> None:
"""LocatorAssertions.not_to_have_css
Expand All @@ -14124,13 +14125,15 @@ async def not_to_have_css(
CSS property name.
value : Union[Pattern[str], str]
CSS property value.
pseudo : Union["after", "before", None]
Pseudo-element to read computed styles from.
timeout : Union[float, None]
Time to retry the assertion for in milliseconds. Defaults to `5000`.
"""
__tracebackhide__ = True

await self._impl_obj.not_to_have_css(
name=name, value=value, timeout=to_milliseconds(timeout)
name=name, value=value, pseudo=pseudo, timeout=to_milliseconds(timeout)
)

async def not_to_have_id(
Expand Down
5 changes: 4 additions & 1 deletion playwright/sync_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14184,6 +14184,7 @@ def not_to_have_css(
name: str,
value: typing.Union[str, typing.Pattern[str]],
*,
pseudo: typing.Optional[Literal["after", "before"]] = None,
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
) -> None:
"""LocatorAssertions.not_to_have_css
Expand All @@ -14196,14 +14197,16 @@ def not_to_have_css(
CSS property name.
value : Union[Pattern[str], str]
CSS property value.
pseudo : Union["after", "before", None]
Pseudo-element to read computed styles from.
timeout : Union[float, None]
Time to retry the assertion for in milliseconds. Defaults to `5000`.
"""
__tracebackhide__ = True

self._sync(
self._impl_obj.not_to_have_css(
name=name, value=value, timeout=to_milliseconds(timeout)
name=name, value=value, pseudo=pseudo, timeout=to_milliseconds(timeout)
)
)

Expand Down
20 changes: 20 additions & 0 deletions tests/async/test_expect_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,23 @@ async def test_should_have_timeout_error_name(page: Page) -> None:
with pytest.raises(TimeoutError) as exc_info:
await page.wait_for_selector("#not-found", timeout=1)
assert exc_info.value.name == "TimeoutError"


async def test_to_have_css_should_support_pseudo_element(page: Page) -> None:
await page.set_content(
"""
<style>
#node::before {
color: rgb(255, 0, 0);
content: "Text content";
}
</style>
<div id=node></div>
"""
)
locator = page.locator("#node")
await expect(locator).to_have_css("color", "rgb(255, 0, 0)", pseudo="before")
with pytest.raises(AssertionError):
await expect(locator).not_to_have_css(
"color", "rgb(255, 0, 0)", pseudo="before", timeout=100
)
20 changes: 20 additions & 0 deletions tests/sync/test_expect_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ def test_to_be_in_viewport_should_report_intersection_even_if_fully_covered_by_o
"""
)
expect(page.locator("h1")).to_be_in_viewport()


def test_to_have_css_should_support_pseudo_element(page: Page) -> None:
page.set_content(
"""
<style>
#node::before {
color: rgb(255, 0, 0);
content: "Text content";
}
</style>
<div id=node></div>
"""
)
locator = page.locator("#node")
expect(locator).to_have_css("color", "rgb(255, 0, 0)", pseudo="before")
with pytest.raises(AssertionError):
expect(locator).not_to_have_css(
"color", "rgb(255, 0, 0)", pseudo="before", timeout=100
)
Loading