From 6bc16180d17f6dd24ed10cefa87aeb290a1ac6f3 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 16 Jul 2026 10:40:20 +0200 Subject: [PATCH] feat(assertions): support attribute presence checks Fixes: https://github.com/microsoft/playwright-python/issues/3133 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 861a91ea-4320-4455-adfe-ad164f6f0fc4 --- playwright/_impl/_assertions.py | 13 +++++++++++-- playwright/async_api/_generated.py | 14 ++++++++------ playwright/sync_api/_generated.py | 14 ++++++++------ scripts/generate_api.py | 2 ++ tests/async/test_assertions.py | 23 ++++++++++++----------- tests/sync/test_assertions.py | 17 ++++++++++++----- 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 47b4e2d8b..30638422d 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -329,11 +329,20 @@ async def not_to_contain_text( async def to_have_attribute( self, name: str, - value: Union[str, Pattern[str]], + value: Union[Pattern[str], str] = None, ignoreCase: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True + if value is None: + await self._expect_impl( + "to.have.attribute", + FrameExpectOptions(expressionArg=name, timeout=timeout), + None, + "Locator expected to have attribute", + 'Expect "to_have_attribute"', + ) + return expected_text = to_expected_text_values([value], ignoreCase=ignoreCase) await self._expect_impl( "to.have.attribute.value", @@ -348,7 +357,7 @@ async def to_have_attribute( async def not_to_have_attribute( self, name: str, - value: Union[str, Pattern[str]], + value: Union[Pattern[str], str] = None, ignoreCase: bool = None, timeout: float = None, ) -> None: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 6dfd634a3..2e8f89a9e 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -21684,7 +21684,7 @@ async def not_to_contain_text( async def to_have_attribute( self, name: str, - value: typing.Union[str, typing.Pattern[str]], + value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, *, ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, @@ -21700,14 +21700,16 @@ async def to_have_attribute( locator = page.locator(\"input\") await expect(locator).to_have_attribute(\"type\", \"text\") + await expect(locator).to_have_attribute(\"disabled\") + await expect(locator).not_to_have_attribute(\"readonly\") ``` Parameters ---------- name : str Attribute name. - value : Union[Pattern[str], str] - Expected attribute value. + value : Union[Pattern[str], str, None] + Expected attribute value. If not specified, the assertion verifies that the attribute is present. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular expression flag if specified. @@ -21728,7 +21730,7 @@ async def to_have_attribute( async def not_to_have_attribute( self, name: str, - value: typing.Union[str, typing.Pattern[str]], + value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, *, ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, @@ -21741,8 +21743,8 @@ async def not_to_have_attribute( ---------- name : str Attribute name. - value : Union[Pattern[str], str] - Expected attribute value. + value : Union[Pattern[str], str, None] + Expected attribute value. If not specified, the assertion verifies that the attribute is absent. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular expression flag if specified. diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index f9d9287ae..61655be99 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -21758,7 +21758,7 @@ def not_to_contain_text( def to_have_attribute( self, name: str, - value: typing.Union[str, typing.Pattern[str]], + value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, *, ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, @@ -21774,14 +21774,16 @@ def to_have_attribute( locator = page.locator(\"input\") expect(locator).to_have_attribute(\"type\", \"text\") + expect(locator).to_have_attribute(\"disabled\") + expect(locator).not_to_have_attribute(\"readonly\") ``` Parameters ---------- name : str Attribute name. - value : Union[Pattern[str], str] - Expected attribute value. + value : Union[Pattern[str], str, None] + Expected attribute value. If not specified, the assertion verifies that the attribute is present. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular expression flag if specified. @@ -21804,7 +21806,7 @@ def to_have_attribute( def not_to_have_attribute( self, name: str, - value: typing.Union[str, typing.Pattern[str]], + value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, *, ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None, @@ -21817,8 +21819,8 @@ def not_to_have_attribute( ---------- name : str Attribute name. - value : Union[Pattern[str], str] - Expected attribute value. + value : Union[Pattern[str], str, None] + Expected attribute value. If not specified, the assertion verifies that the attribute is absent. ignore_case : Union[bool, None] Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular expression flag if specified. diff --git a/scripts/generate_api.py b/scripts/generate_api.py index fb0579f1f..95a0391f2 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -104,10 +104,12 @@ def process_type(value: Any, param: bool = False) -> str: r"expect_.*\.predicate", r"evaluate_handle\.arg", r"frame.*\.name", + r"not_to_have_attribute\.value", r"register\.script", r"select_option\.value", r"send\.params", r"set_geolocation\.geolocation", + r"to_have_attribute\.value", r"wait_for_.*\.predicate", r"wait_for_load_state\.state", r"unroute\.handler", diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 3fd403ac1..7c06f6ef2 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -110,18 +110,19 @@ async def test_assertions_locator_to_contain_text_should_throw_if_arg_is_unsuppo async def test_assertions_locator_to_have_attribute(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) - await page.set_content("
kek
") - await expect(page.locator("div#foobar")).to_have_attribute("id", "foobar") - await expect(page.locator("div#foobar")).to_have_attribute( - "id", re.compile("foobar") - ) - await expect(page.locator("div#foobar")).not_to_have_attribute( - "id", "kek", timeout=100 - ) + await page.set_content("
kek
") + locator = page.locator("div#foobar") + await expect(locator).to_have_attribute("checked") + await expect(locator).not_to_have_attribute("open", timeout=100) + await expect(locator).to_have_attribute("id", "foobar") + await expect(locator).to_have_attribute("id", re.compile("foobar")) + await expect(locator).not_to_have_attribute("id", "kek", timeout=100) with pytest.raises(AssertionError): - await expect(page.locator("div#foobar")).to_have_attribute( - "id", "koko", timeout=100 - ) + await expect(locator).to_have_attribute("open", timeout=100) + with pytest.raises(AssertionError): + await expect(locator).not_to_have_attribute("checked", timeout=100) + with pytest.raises(AssertionError): + await expect(locator).to_have_attribute("id", "koko", timeout=100) async def test_assertions_locator_to_have_attribute_ignore_case( diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index ed58cdcdd..014ffb7b2 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -95,12 +95,19 @@ def test_assertions_locator_to_contain_text(page: Page, server: Server) -> None: def test_assertions_locator_to_have_attribute(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) - page.set_content("
kek
") - expect(page.locator("div#foobar")).to_have_attribute("id", "foobar") - expect(page.locator("div#foobar")).to_have_attribute("id", re.compile("foobar")) - expect(page.locator("div#foobar")).not_to_have_attribute("id", "kek", timeout=100) + page.set_content("
kek
") + locator = page.locator("div#foobar") + expect(locator).to_have_attribute("checked") + expect(locator).not_to_have_attribute("open", timeout=100) + expect(locator).to_have_attribute("id", "foobar") + expect(locator).to_have_attribute("id", re.compile("foobar")) + expect(locator).not_to_have_attribute("id", "kek", timeout=100) + with pytest.raises(AssertionError): + expect(locator).to_have_attribute("open", timeout=100) + with pytest.raises(AssertionError): + expect(locator).not_to_have_attribute("checked", timeout=100) with pytest.raises(AssertionError): - expect(page.locator("div#foobar")).to_have_attribute("id", "koko", timeout=100) + expect(locator).to_have_attribute("id", "koko", timeout=100) def test_assertions_locator_to_have_attribute_ignore_case(