diff --git a/.claude/skills/playwright-release/SKILL.md b/.claude/skills/playwright-release/SKILL.md index df8562fcc..63abd8cb6 100644 --- a/.claude/skills/playwright-release/SKILL.md +++ b/.claude/skills/playwright-release/SKILL.md @@ -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: diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index 597f5d003..27dbc7553 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -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. diff --git a/CLAUDE.md b/AGENTS.md similarity index 79% rename from CLAUDE.md rename to AGENTS.md index f5373d463..df3537212 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -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 @@ -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--.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). @@ -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: diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index d3ec27215..4feacf259 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -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, diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 000396c9e..6dd97eb99 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -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 @@ -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( diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 85cf04039..794b48cbd 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -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 @@ -14196,6 +14197,8 @@ 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`. """ @@ -14203,7 +14206,7 @@ def not_to_have_css( 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) ) ) diff --git a/tests/async/test_expect_misc.py b/tests/async/test_expect_misc.py index 2148b0d9e..d50747888 100644 --- a/tests/async/test_expect_misc.py +++ b/tests/async/test_expect_misc.py @@ -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( + """ + +
+ """ + ) + 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 + ) diff --git a/tests/sync/test_expect_misc.py b/tests/sync/test_expect_misc.py index 4d8b68cc0..1e3eb8447 100644 --- a/tests/sync/test_expect_misc.py +++ b/tests/sync/test_expect_misc.py @@ -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( + """ + +
+ """ + ) + 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 + )