Skip to content

Commit e282735

Browse files
Merge pull request #34 from valentinogagliardi/select-role
Implement select role lookup
2 parents a844d8c + f7a4b62 commit e282735

File tree

6 files changed

+91
-20
lines changed

6 files changed

+91
-20
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
project = "unbrowsed"
1111
copyright = "2025, Valentino Gagliardi"
1212
author = "Valentino Gagliardi"
13-
release = "0.1.0a19"
13+
release = "0.1.0a20"
1414

1515
# -- General configuration ---------------------------------------------------
1616
extensions = [

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ requires = [ "setuptools>=61" ]
55

66
[project]
77
name = "unbrowsed"
8-
version = "0.1.0a19"
8+
version = "0.1.0a20"
99
description = "A browserless HTML testing library for Python"
1010
readme = "README.md"
1111
license = { file = "LICENSE" }

src/unbrowsed/queries.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""unbrowsed queries."""
22

3-
from typing import Any, Optional, Union
3+
from typing import Any, Optional
44

55
from selectolax.lexbor import LexborHTMLParser as Parser
66
from selectolax.lexbor import LexborNode
@@ -243,7 +243,7 @@ def get_by_text(dom: Parser, text: str, exact=True) -> QueryResult:
243243
def query_by_role(
244244
dom: Parser,
245245
role: AriaRoles,
246-
current: Optional[Union[bool, str]] = None,
246+
current: Optional[bool | str] = None,
247247
name: Optional[str] = None,
248248
description: Optional[str] = None,
249249
) -> Optional[QueryResult]:
@@ -315,7 +315,7 @@ def query_by_role(
315315
def get_by_role(
316316
dom: Parser,
317317
role: AriaRoles,
318-
current: Optional[Union[bool, str]] = None,
318+
current: Optional[bool | str] = None,
319319
name: Optional[str] = None,
320320
description: Optional[str] = None,
321321
) -> QueryResult:
@@ -367,7 +367,7 @@ def get_by_role(
367367

368368

369369
def query_all_by_role(
370-
dom: Parser, role: AriaRoles, current: Optional[Union[bool, str]] = None
370+
dom: Parser, role: AriaRoles, current: Optional[bool | str] = None
371371
) -> list[QueryResult]:
372372
"""
373373
Queries the DOM for all elements with the specified ARIA role.
@@ -402,7 +402,7 @@ def query_all_by_role(
402402

403403

404404
def get_all_by_role(
405-
dom: Parser, role: AriaRoles, current: Optional[Union[bool, str]] = None
405+
dom: Parser, role: AriaRoles, current: Optional[bool | str] = None
406406
) -> list[QueryResult]:
407407
"""
408408
Retrieves all elements from the DOM by their ARIA role.

src/unbrowsed/resolvers.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ def __init__(
113113
@staticmethod
114114
def get_implicit_role_mapping() -> ImplicitRoleMapping:
115115
return {
116-
"a": lambda node: (
117-
"link" if "href" in node.attributes else "generic"
118-
),
116+
"a": RoleResolver.get_a_role,
119117
"article": "article",
120118
"aside": "complementary",
121119
"address": "group",
@@ -133,7 +131,7 @@ def get_implicit_role_mapping() -> ImplicitRoleMapping:
133131
"password": "textbox",
134132
},
135133
"textarea": "textbox",
136-
"select": "combobox",
134+
"select": RoleResolver.get_select_role,
137135
"nav": "navigation",
138136
"main": "main",
139137
"meter": "meter",
@@ -195,14 +193,14 @@ def get_implicit_role(node: LexborNode):
195193
return handler if isinstance(handler, str) else None
196194

197195
@staticmethod
198-
def get_td_role(node: LexborNode) -> Optional[str]:
196+
def get_td_role(node: LexborNode) -> str:
199197
"""Determine the role of a td element."""
200198
ancestor = node.parent
201199
while ancestor and ancestor.tag != "table":
202200
ancestor = ancestor.parent
203201

204202
if not ancestor:
205-
return None
203+
return ""
206204

207205
table_role = ancestor.attributes.get("role")
208206

@@ -215,10 +213,10 @@ def get_td_role(node: LexborNode) -> Optional[str]:
215213
elif table_role in ["grid", "treegrid"]:
216214
return "gridcell"
217215

218-
return None
216+
return ""
219217

220218
@staticmethod
221-
def get_img_role(node: LexborNode) -> Optional[str]:
219+
def get_img_role(node: LexborNode) -> str:
222220
"""Determine the role of an img element."""
223221
if "alt" in node.attributes:
224222
alt = node.attributes.get("alt")
@@ -231,3 +229,23 @@ def get_img_role(node: LexborNode) -> Optional[str]:
231229
):
232230
return "presentation"
233231
return "img"
232+
233+
@staticmethod
234+
def get_select_role(node: LexborNode) -> str:
235+
"""Determine the role of a select element."""
236+
if "multiple" in node.attributes:
237+
return "listbox"
238+
if (
239+
"size" in node.attributes
240+
and node.attributes.get("size", None) is not None
241+
):
242+
if int(node.attributes.get("size")) > 1: # type: ignore
243+
return "listbox"
244+
return "combobox"
245+
246+
@staticmethod
247+
def get_a_role(node: LexborNode) -> str:
248+
"""Determine the role of an element"""
249+
if "href" in node.attributes:
250+
return "link"
251+
return "generic"

src/unbrowsed/types.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from typing import Literal, TypedDict, Callable
1+
from typing import Literal, TypedDict
2+
from collections.abc import Callable
3+
from selectolax.lexbor import LexborNode
24

35
Alert = Literal["alert"]
46
Article = Literal["article"]
@@ -104,7 +106,7 @@ class InputType(TypedDict):
104106

105107

106108
class ImplicitRoleMapping(TypedDict, total=False):
107-
a: Callable
109+
a: Callable[[LexborNode], str]
108110
article: Article
109111
address: Group
110112
aside: Complementary
@@ -128,7 +130,7 @@ class ImplicitRoleMapping(TypedDict, total=False):
128130
h5: Heading
129131
h6: Heading
130132
hr: Separator
131-
img: Callable
133+
img: Callable[[LexborNode], str]
132134
input: InputType
133135
li: ListItem
134136
main: Main
@@ -141,11 +143,11 @@ class ImplicitRoleMapping(TypedDict, total=False):
141143
output: Status
142144
progress: ProgressBar
143145
section: Region
144-
select: ComboBox
146+
select: Callable[[LexborNode], str]
145147
summary: Button
146148
table: Table
147149
tbody: RowGroup
148-
td: Callable
150+
td: Callable[[LexborNode], str]
149151
textarea: TextBox
150152
tfoot: RowGroup
151153
th: ColumnHeader

tests/test_get_by_role.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,54 @@ def test_get_by_role_img_no_accessible_name():
682682
"""
683683
dom = parse_html(html)
684684
get_by_role(dom, "presentation")
685+
686+
687+
def test_gey_by_role_select():
688+
html = """
689+
<label for="pet-select">Choose a pet:</label>
690+
691+
<select name="pets" id="pet-select">
692+
<option value="">--Please choose an option--</option>
693+
<option value="dog">Dog</option>
694+
<option value="cat">Cat</option>
695+
<option value="hamster">Hamster</option>
696+
<option value="parrot">Parrot</option>
697+
<option value="spider">Spider</option>
698+
<option value="goldfish">Goldfish</option>
699+
</select>
700+
"""
701+
dom = parse_html(html)
702+
get_by_role(dom, "combobox")
703+
704+
html = """
705+
<label for="shakes">Which shakes would you like to order?</label>
706+
<select id="shakes" name="shakes" size>
707+
<option>Vanilla Shake</option>
708+
<option>Strawberry Shake</option>
709+
<option>Chocolate Shake</option>
710+
</select>
711+
"""
712+
dom = parse_html(html)
713+
get_by_role(dom, "combobox")
714+
715+
html = """
716+
<label for="shakes">Which shakes would you like to order?</label>
717+
<select id="shakes" name="shakes" size="2">
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, "listbox")
725+
726+
html = """
727+
<label for="shakes">Which shakes would you like to order?</label>
728+
<select id="shakes" name="shakes" multiple>
729+
<option>Vanilla Shake</option>
730+
<option>Strawberry Shake</option>
731+
<option>Chocolate Shake</option>
732+
</select>
733+
"""
734+
dom = parse_html(html)
735+
get_by_role(dom, "listbox")

0 commit comments

Comments
 (0)