Skip to content

Commit e5dbb4a

Browse files
authored
fix(assertions): accept pseudo in not_to_have_css (#3261)
1 parent ac0dd3c commit e5dbb4a

8 files changed

Lines changed: 75 additions & 9 deletions

File tree

‎.claude/skills/playwright-release/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Open a PR from the branch with the roll commit:
106106
release version, even when `DRIVER_VERSION` is a beta build.
107107
- Description: the release notes from step 2, in GitHub markdown (see below).
108108
The user copies this description into the GitHub release, so for this PR it
109-
replaces the "keep PR descriptions short" rule in `CLAUDE.md`.
109+
replaces the "keep PR descriptions short" rule in `AGENTS.md`.
110110

111111
Convert the notes to GitHub markdown yourself:
112112

‎.claude/skills/playwright-roll/SKILL.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,11 @@ After the script settles, run `pre-commit run --all-files` once more to confirm
281281

282282
### 6. Add tests
283283

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

286-
- 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.
286+
- 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.
287+
- 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`.
288+
- When no upstream test covers the change, extend an existing test before you add a new one.
287289
- 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`).
288290
- For event-info objects, `await message_info.value` (it's an `async` property).
289291
- 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.

CLAUDE.md renamed to AGENTS.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# CLAUDE.md
1+
# AGENTS.md
22

3-
Guidance for Claude when working in this repository.
3+
Guidance for coding agents working in this repository.
44

55
## What this is
66

@@ -14,7 +14,7 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
1414
- `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.
1515
- `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.
1616
- `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.
17-
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
17+
- `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).
1818
- `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.
1919
- `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`).
2020
- `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
4545

4646
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`.
4747

48+
## Tests
49+
50+
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.
51+
52+
**First rule: follow the upstream names and files.**
53+
54+
- 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.
55+
- 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.
56+
- 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`.
57+
- Add the sync mirror in `tests/sync/`, with the same file name and test name.
58+
59+
**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).
60+
61+
- **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.
62+
- **New features**: cover the mainstream scenario and the important edge cases only. Do not create matrices of tests for every scenario, option, or aspect.
63+
- **Regressions**: generalize the provided regression cases into meaningful tests, and stop there. Do not add more tests around them.
64+
4865
## Rolling Playwright to a new version
4966

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

‎playwright/_impl/_assertions.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,9 +505,10 @@ async def not_to_have_css(
505505
name: str,
506506
value: Union[str, Pattern[str]],
507507
timeout: float = None,
508+
pseudo: Literal["after", "before"] = None,
508509
) -> None:
509510
__tracebackhide__ = True
510-
await self._not.to_have_css(name, value, timeout)
511+
await self._not.to_have_css(name, value, timeout, pseudo)
511512

512513
async def to_have_id(
513514
self,

‎playwright/async_api/_generated.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14112,6 +14112,7 @@ async def not_to_have_css(
1411214112
name: str,
1411314113
value: typing.Union[str, typing.Pattern[str]],
1411414114
*,
14115+
pseudo: typing.Optional[Literal["after", "before"]] = None,
1411514116
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
1411614117
) -> None:
1411714118
"""LocatorAssertions.not_to_have_css
@@ -14124,13 +14125,15 @@ async def not_to_have_css(
1412414125
CSS property name.
1412514126
value : Union[Pattern[str], str]
1412614127
CSS property value.
14128+
pseudo : Union["after", "before", None]
14129+
Pseudo-element to read computed styles from.
1412714130
timeout : Union[float, None]
1412814131
Time to retry the assertion for in milliseconds. Defaults to `5000`.
1412914132
"""
1413014133
__tracebackhide__ = True
1413114134

1413214135
await self._impl_obj.not_to_have_css(
14133-
name=name, value=value, timeout=to_milliseconds(timeout)
14136+
name=name, value=value, pseudo=pseudo, timeout=to_milliseconds(timeout)
1413414137
)
1413514138

1413614139
async def not_to_have_id(

‎playwright/sync_api/_generated.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14184,6 +14184,7 @@ def not_to_have_css(
1418414184
name: str,
1418514185
value: typing.Union[str, typing.Pattern[str]],
1418614186
*,
14187+
pseudo: typing.Optional[Literal["after", "before"]] = None,
1418714188
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
1418814189
) -> None:
1418914190
"""LocatorAssertions.not_to_have_css
@@ -14196,14 +14197,16 @@ def not_to_have_css(
1419614197
CSS property name.
1419714198
value : Union[Pattern[str], str]
1419814199
CSS property value.
14200+
pseudo : Union["after", "before", None]
14201+
Pseudo-element to read computed styles from.
1419914202
timeout : Union[float, None]
1420014203
Time to retry the assertion for in milliseconds. Defaults to `5000`.
1420114204
"""
1420214205
__tracebackhide__ = True
1420314206

1420414207
self._sync(
1420514208
self._impl_obj.not_to_have_css(
14206-
name=name, value=value, timeout=to_milliseconds(timeout)
14209+
name=name, value=value, pseudo=pseudo, timeout=to_milliseconds(timeout)
1420714210
)
1420814211
)
1420914212

‎tests/async/test_expect_misc.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,23 @@ async def test_should_have_timeout_error_name(page: Page) -> None:
7878
with pytest.raises(TimeoutError) as exc_info:
7979
await page.wait_for_selector("#not-found", timeout=1)
8080
assert exc_info.value.name == "TimeoutError"
81+
82+
83+
async def test_to_have_css_should_support_pseudo_element(page: Page) -> None:
84+
await page.set_content(
85+
"""
86+
<style>
87+
#node::before {
88+
color: rgb(255, 0, 0);
89+
content: "Text content";
90+
}
91+
</style>
92+
<div id=node></div>
93+
"""
94+
)
95+
locator = page.locator("#node")
96+
await expect(locator).to_have_css("color", "rgb(255, 0, 0)", pseudo="before")
97+
with pytest.raises(AssertionError):
98+
await expect(locator).not_to_have_css(
99+
"color", "rgb(255, 0, 0)", pseudo="before", timeout=100
100+
)

‎tests/sync/test_expect_misc.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,23 @@ def test_to_be_in_viewport_should_report_intersection_even_if_fully_covered_by_o
7070
"""
7171
)
7272
expect(page.locator("h1")).to_be_in_viewport()
73+
74+
75+
def test_to_have_css_should_support_pseudo_element(page: Page) -> None:
76+
page.set_content(
77+
"""
78+
<style>
79+
#node::before {
80+
color: rgb(255, 0, 0);
81+
content: "Text content";
82+
}
83+
</style>
84+
<div id=node></div>
85+
"""
86+
)
87+
locator = page.locator("#node")
88+
expect(locator).to_have_css("color", "rgb(255, 0, 0)", pseudo="before")
89+
with pytest.raises(AssertionError):
90+
expect(locator).not_to_have_css(
91+
"color", "rgb(255, 0, 0)", pseudo="before", timeout=100
92+
)

0 commit comments

Comments
 (0)