Skip to content

Commit 55fa500

Browse files
authored
feat(assertions): support attribute presence checks (#3136)
1 parent 1dc5231 commit 55fa500

6 files changed

Lines changed: 53 additions & 30 deletions

File tree

playwright/_impl/_assertions.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,20 @@ async def not_to_contain_text(
329329
async def to_have_attribute(
330330
self,
331331
name: str,
332-
value: Union[str, Pattern[str]],
332+
value: Union[Pattern[str], str] = None,
333333
ignoreCase: bool = None,
334334
timeout: float = None,
335335
) -> None:
336336
__tracebackhide__ = True
337+
if value is None:
338+
await self._expect_impl(
339+
"to.have.attribute",
340+
FrameExpectOptions(expressionArg=name, timeout=timeout),
341+
None,
342+
"Locator expected to have attribute",
343+
'Expect "to_have_attribute"',
344+
)
345+
return
337346
expected_text = to_expected_text_values([value], ignoreCase=ignoreCase)
338347
await self._expect_impl(
339348
"to.have.attribute.value",
@@ -348,7 +357,7 @@ async def to_have_attribute(
348357
async def not_to_have_attribute(
349358
self,
350359
name: str,
351-
value: Union[str, Pattern[str]],
360+
value: Union[Pattern[str], str] = None,
352361
ignoreCase: bool = None,
353362
timeout: float = None,
354363
) -> None:

playwright/async_api/_generated.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21684,7 +21684,7 @@ async def not_to_contain_text(
2168421684
async def to_have_attribute(
2168521685
self,
2168621686
name: str,
21687-
value: typing.Union[str, typing.Pattern[str]],
21687+
value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None,
2168821688
*,
2168921689
ignore_case: typing.Optional[bool] = None,
2169021690
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
@@ -21700,14 +21700,16 @@ async def to_have_attribute(
2170021700

2170121701
locator = page.locator(\"input\")
2170221702
await expect(locator).to_have_attribute(\"type\", \"text\")
21703+
await expect(locator).to_have_attribute(\"disabled\")
21704+
await expect(locator).not_to_have_attribute(\"readonly\")
2170321705
```
2170421706

2170521707
Parameters
2170621708
----------
2170721709
name : str
2170821710
Attribute name.
21709-
value : Union[Pattern[str], str]
21710-
Expected attribute value.
21711+
value : Union[Pattern[str], str, None]
21712+
Expected attribute value. If not specified, the assertion verifies that the attribute is present.
2171121713
ignore_case : Union[bool, None]
2171221714
Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular
2171321715
expression flag if specified.
@@ -21728,7 +21730,7 @@ async def to_have_attribute(
2172821730
async def not_to_have_attribute(
2172921731
self,
2173021732
name: str,
21731-
value: typing.Union[str, typing.Pattern[str]],
21733+
value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None,
2173221734
*,
2173321735
ignore_case: typing.Optional[bool] = None,
2173421736
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
@@ -21741,8 +21743,8 @@ async def not_to_have_attribute(
2174121743
----------
2174221744
name : str
2174321745
Attribute name.
21744-
value : Union[Pattern[str], str]
21745-
Expected attribute value.
21746+
value : Union[Pattern[str], str, None]
21747+
Expected attribute value. If not specified, the assertion verifies that the attribute is absent.
2174621748
ignore_case : Union[bool, None]
2174721749
Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular
2174821750
expression flag if specified.

playwright/sync_api/_generated.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21758,7 +21758,7 @@ def not_to_contain_text(
2175821758
def to_have_attribute(
2175921759
self,
2176021760
name: str,
21761-
value: typing.Union[str, typing.Pattern[str]],
21761+
value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None,
2176221762
*,
2176321763
ignore_case: typing.Optional[bool] = None,
2176421764
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
@@ -21774,14 +21774,16 @@ def to_have_attribute(
2177421774

2177521775
locator = page.locator(\"input\")
2177621776
expect(locator).to_have_attribute(\"type\", \"text\")
21777+
expect(locator).to_have_attribute(\"disabled\")
21778+
expect(locator).not_to_have_attribute(\"readonly\")
2177721779
```
2177821780

2177921781
Parameters
2178021782
----------
2178121783
name : str
2178221784
Attribute name.
21783-
value : Union[Pattern[str], str]
21784-
Expected attribute value.
21785+
value : Union[Pattern[str], str, None]
21786+
Expected attribute value. If not specified, the assertion verifies that the attribute is present.
2178521787
ignore_case : Union[bool, None]
2178621788
Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular
2178721789
expression flag if specified.
@@ -21804,7 +21806,7 @@ def to_have_attribute(
2180421806
def not_to_have_attribute(
2180521807
self,
2180621808
name: str,
21807-
value: typing.Union[str, typing.Pattern[str]],
21809+
value: typing.Optional[typing.Union[typing.Pattern[str], str]] = None,
2180821810
*,
2180921811
ignore_case: typing.Optional[bool] = None,
2181021812
timeout: typing.Optional[typing.Union[float, datetime.timedelta]] = None,
@@ -21817,8 +21819,8 @@ def not_to_have_attribute(
2181721819
----------
2181821820
name : str
2181921821
Attribute name.
21820-
value : Union[Pattern[str], str]
21821-
Expected attribute value.
21822+
value : Union[Pattern[str], str, None]
21823+
Expected attribute value. If not specified, the assertion verifies that the attribute is absent.
2182221824
ignore_case : Union[bool, None]
2182321825
Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular
2182421826
expression flag if specified.

scripts/generate_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,12 @@ def process_type(value: Any, param: bool = False) -> str:
104104
r"expect_.*\.predicate",
105105
r"evaluate_handle\.arg",
106106
r"frame.*\.name",
107+
r"not_to_have_attribute\.value",
107108
r"register\.script",
108109
r"select_option\.value",
109110
r"send\.params",
110111
r"set_geolocation\.geolocation",
112+
r"to_have_attribute\.value",
111113
r"wait_for_.*\.predicate",
112114
r"wait_for_load_state\.state",
113115
r"unroute\.handler",

tests/async/test_assertions.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,19 @@ async def test_assertions_locator_to_contain_text_should_throw_if_arg_is_unsuppo
110110

111111
async def test_assertions_locator_to_have_attribute(page: Page, server: Server) -> None:
112112
await page.goto(server.EMPTY_PAGE)
113-
await page.set_content("<div id=foobar>kek</div>")
114-
await expect(page.locator("div#foobar")).to_have_attribute("id", "foobar")
115-
await expect(page.locator("div#foobar")).to_have_attribute(
116-
"id", re.compile("foobar")
117-
)
118-
await expect(page.locator("div#foobar")).not_to_have_attribute(
119-
"id", "kek", timeout=100
120-
)
113+
await page.set_content("<div id=foobar checked>kek</div>")
114+
locator = page.locator("div#foobar")
115+
await expect(locator).to_have_attribute("checked")
116+
await expect(locator).not_to_have_attribute("open", timeout=100)
117+
await expect(locator).to_have_attribute("id", "foobar")
118+
await expect(locator).to_have_attribute("id", re.compile("foobar"))
119+
await expect(locator).not_to_have_attribute("id", "kek", timeout=100)
121120
with pytest.raises(AssertionError):
122-
await expect(page.locator("div#foobar")).to_have_attribute(
123-
"id", "koko", timeout=100
124-
)
121+
await expect(locator).to_have_attribute("open", timeout=100)
122+
with pytest.raises(AssertionError):
123+
await expect(locator).not_to_have_attribute("checked", timeout=100)
124+
with pytest.raises(AssertionError):
125+
await expect(locator).to_have_attribute("id", "koko", timeout=100)
125126

126127

127128
async def test_assertions_locator_to_have_attribute_ignore_case(

tests/sync/test_assertions.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,19 @@ def test_assertions_locator_to_contain_text(page: Page, server: Server) -> None:
9595

9696
def test_assertions_locator_to_have_attribute(page: Page, server: Server) -> None:
9797
page.goto(server.EMPTY_PAGE)
98-
page.set_content("<div id=foobar>kek</div>")
99-
expect(page.locator("div#foobar")).to_have_attribute("id", "foobar")
100-
expect(page.locator("div#foobar")).to_have_attribute("id", re.compile("foobar"))
101-
expect(page.locator("div#foobar")).not_to_have_attribute("id", "kek", timeout=100)
98+
page.set_content("<div id=foobar checked>kek</div>")
99+
locator = page.locator("div#foobar")
100+
expect(locator).to_have_attribute("checked")
101+
expect(locator).not_to_have_attribute("open", timeout=100)
102+
expect(locator).to_have_attribute("id", "foobar")
103+
expect(locator).to_have_attribute("id", re.compile("foobar"))
104+
expect(locator).not_to_have_attribute("id", "kek", timeout=100)
105+
with pytest.raises(AssertionError):
106+
expect(locator).to_have_attribute("open", timeout=100)
107+
with pytest.raises(AssertionError):
108+
expect(locator).not_to_have_attribute("checked", timeout=100)
102109
with pytest.raises(AssertionError):
103-
expect(page.locator("div#foobar")).to_have_attribute("id", "koko", timeout=100)
110+
expect(locator).to_have_attribute("id", "koko", timeout=100)
104111

105112

106113
def test_assertions_locator_to_have_attribute_ignore_case(

0 commit comments

Comments
 (0)