Skip to content

Commit 276a580

Browse files
Merge pull request #46 from valentinogagliardi/maintenance/body-html
Simplify traversal; increase coverage
2 parents e282735 + 2d7a475 commit 276a580

File tree

6 files changed

+56
-46
lines changed

6 files changed

+56
-46
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ classifiers = [
1818
"Intended Audience :: Developers",
1919
"License :: OSI Approved :: MIT License",
2020
"Programming Language :: Python :: 3 :: Only",
21+
"Programming Language :: Python :: 3.8",
22+
"Programming Language :: Python :: 3.9",
2123
"Programming Language :: Python :: 3.10",
2224
"Programming Language :: Python :: 3.11",
2325
"Programming Language :: Python :: 3.12",

src/unbrowsed/queries.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from unbrowsed.types import AriaRoles
1515
from unbrowsed.resolvers import RoleResolver
1616

17+
SELECTOR = "*:not(html):not(body)"
18+
1719

1820
class QueryResult:
1921
"""Wrapper class for query result."""
@@ -170,29 +172,22 @@ def query_by_text(dom: Parser, text: str, exact=True) -> Optional[QueryResult]:
170172
search_text = TextMatch(text, exact=exact)
171173
matches = []
172174

173-
for element in dom.css("*"):
175+
for element in dom.css(SELECTOR):
174176
element_text = element.text(deep=True, strip=True)
175177

176178
if search_text.matches(element_text):
177179
matches.append(element)
178180

179181
if len(matches) > 1:
180-
exclusions = ("html", "body")
181-
filtered_matches = [m for m in matches if m.tag not in exclusions]
182-
183-
if filtered_matches:
184-
matches = filtered_matches
185-
186-
if len(matches) > 1:
187-
for i, parent in enumerate(matches):
188-
for j, child in enumerate(matches):
189-
if i != j and is_parent_of(parent, child):
190-
return QueryResult(matches[i])
182+
for i, parent in enumerate(matches):
183+
for j, child in enumerate(matches):
184+
if i != j and is_parent_of(parent, child):
185+
return QueryResult(matches[i])
191186

192-
raise MultipleElementsFoundError(
193-
f"Found {len(matches)} elements with text '{text}'. "
194-
f"Use query_all_by_text if multiple matches are expected."
195-
)
187+
raise MultipleElementsFoundError(
188+
f"Found {len(matches)} elements with text '{text}'. "
189+
f"Use query_all_by_text if multiple matches are expected."
190+
)
196191

197192
if not matches:
198193
return None
@@ -276,7 +271,7 @@ def query_by_role(
276271
)
277272
matches = []
278273

279-
for element in dom.css("*"):
274+
for element in dom.css(SELECTOR):
280275
if not role_matcher.matches(element):
281276
continue
282277

@@ -288,13 +283,6 @@ def query_by_role(
288283

289284
matches.append(element)
290285

291-
if len(matches) > 1:
292-
exclusions = ("html", "body")
293-
filtered_matches = [m for m in matches if m.tag not in exclusions]
294-
295-
if filtered_matches:
296-
matches = filtered_matches
297-
298286
if len(matches) > 1:
299287
for i, parent in enumerate(matches):
300288
for j, child in enumerate(matches):

src/unbrowsed/resolvers.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ def resolve(node: LexborNode) -> Optional[str]:
2121
text = element.text(deep=True, strip=True)
2222
if text:
2323
name_texts.append(text)
24-
25-
if name_texts:
26-
return " ".join(name_texts)
24+
return " ".join(name_texts)
2725

2826
if aria_label := node.attributes.get("aria-label"):
2927
aria_label = aria_label.strip()
@@ -63,9 +61,7 @@ def resolve(node: LexborNode) -> Optional[str]:
6361
return f"{alt_text} {node_text}"
6462
return alt_text
6563

66-
content = node.text(deep=True, strip=True)
67-
if content:
68-
return content
64+
return node.text(deep=True, strip=True)
6965

7066
if title := node.attributes.get("title"):
7167
if title.strip():
@@ -176,11 +172,7 @@ def matches(self, node: LexborNode) -> bool:
176172

177173
@staticmethod
178174
def get_implicit_role(node: LexborNode):
179-
if not hasattr(node, "tag"):
180-
return
181175
tag = node.tag
182-
if not tag:
183-
return
184176
handler = RoleResolver.get_implicit_role_mapping().get(
185177
tag # type: ignore
186178
)
@@ -199,10 +191,7 @@ def get_td_role(node: LexborNode) -> str:
199191
while ancestor and ancestor.tag != "table":
200192
ancestor = ancestor.parent
201193

202-
if not ancestor:
203-
return ""
204-
205-
table_role = ancestor.attributes.get("role")
194+
table_role = ancestor.attributes.get("role") # type: ignore
206195

207196
if not table_role:
208197
return "cell"

tests/test_get_by_label_text.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def labels_html():
2121
def test_get_by_label_text(labels_html):
2222
dom = parse_html(labels_html)
2323

24-
assert get_by_label_text(dom, "Email")
24+
get_by_label_text(dom, "Email")
25+
get_by_label_text(dom, "Password")
2526

2627
with pytest.raises(NoElementsFoundError) as exc:
2728
get_by_label_text(dom, "email")
@@ -30,8 +31,6 @@ def test_get_by_label_text(labels_html):
3031
"Use query_by_label_text if expecting no matches." == str(exc.value)
3132
)
3233

33-
assert get_by_label_text(dom, "Password")
34-
3534
html = """
3635
<form>
3736
<label for="email">Email Address</label>
@@ -40,15 +39,15 @@ def test_get_by_label_text(labels_html):
4039
"""
4140
dom = parse_html(html)
4241

43-
assert get_by_label_text(dom, "Email Address")
42+
get_by_label_text(dom, "Email Address")
4443

4544

4645
def test_get_by_label_text_exact(labels_html):
4746
dom = parse_html(labels_html)
4847

49-
assert get_by_label_text(dom, "email", exact=False)
50-
assert get_by_label_text(dom, "password", exact=False)
51-
assert get_by_label_text(dom, "passw", exact=False)
48+
get_by_label_text(dom, "email", exact=False)
49+
get_by_label_text(dom, "password", exact=False)
50+
get_by_label_text(dom, "passw", exact=False)
5251

5352

5453
def test_get_by_label_text_no_match():
@@ -95,4 +94,14 @@ def test_get_by_label_text_nested_control():
9594
"""
9695
dom = parse_html(html)
9796

98-
assert get_by_label_text(dom, "Username")
97+
get_by_label_text(dom, "Username")
98+
99+
100+
def test_no_for_attr():
101+
html = """
102+
<label>Password</label>
103+
<input id="password" type="password">
104+
"""
105+
dom = parse_html(html)
106+
with pytest.raises(NoElementsFoundError):
107+
get_by_label_text(dom, "Password")

tests/test_get_by_role.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,17 @@ def test_gey_by_role_select():
712712
dom = parse_html(html)
713713
get_by_role(dom, "combobox")
714714

715+
html = """
716+
<label for="shakes">Which shakes would you like to order?</label>
717+
<select id="shakes" name="shakes" size="1">
718+
<option>Vanilla Shake</option>
719+
<option>Strawberry Shake</option>
720+
<option>Chocolate Shake</option>
721+
</select>
722+
"""
723+
dom = parse_html(html)
724+
get_by_role(dom, "combobox")
725+
715726
html = """
716727
<label for="shakes">Which shakes would you like to order?</label>
717728
<select id="shakes" name="shakes" size="2">

tests/test_resolvers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,14 @@ def test_role_matcher_input_types():
382382

383383
unknown = dom.css_first('input[type="unknown"]')
384384
assert not RoleResolver.get_implicit_role(unknown)
385+
386+
387+
def test_input_without_label():
388+
html = """
389+
<input id="username" type="text">
390+
<label>Username</label>
391+
"""
392+
parser = LexborHTMLParser(html)
393+
input_element = parser.css_first("input")
394+
395+
assert AccessibleNameResolver.resolve(input_element) is None

0 commit comments

Comments
 (0)