Skip to content

Commit c8b4f21

Browse files
committed
input properties included
1 parent a53bfc3 commit c8b4f21

File tree

4 files changed

+83
-9
lines changed

4 files changed

+83
-9
lines changed

stagehand/utils.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pydantic import AnyUrl, BaseModel, Field, HttpUrl, create_model
66
from pydantic.fields import FieldInfo
77

8-
from stagehand.types.a11y import AccessibilityNode
8+
from stagehand.types.a11y import AccessibilityNode, AXProperty, AXValue
99

1010

1111
def snake_to_camel(snake_str: str) -> str:
@@ -95,11 +95,85 @@ def convert_dict_keys_to_snake_case(data: Any) -> Any:
9595
return data
9696

9797

98+
INCLUDED_NODE_PROPERTY_NAMES = {
99+
"selected",
100+
"checked",
101+
"value",
102+
"valuemin",
103+
"valuemax",
104+
"valuetext",
105+
}
106+
"""
107+
AX Property names included in the simplified tree.
108+
"""
109+
110+
111+
def _format_ax_value(value_type: str, value: AXValue) -> Union[str, None]:
112+
"""
113+
Formats the accessibility value, or returns None if the value is unsupported.
114+
115+
NOTE:
116+
Refer to "Accessible Rich Internet Applications (WAI-ARIA) 1.2"
117+
for details.
118+
https://www.w3.org/TR/wai-aria-1.2/#propcharacteristic_value
119+
"""
120+
if value_type == "tristate" and value in ["true", "mixed"]:
121+
return str(value)
122+
elif value_type == "booleanOrUndefined" and value in [True, "true"]:
123+
return "true"
124+
elif value_type == "number" and isinstance(value, (int, float)):
125+
return str(value)
126+
elif value_type == "string" and value:
127+
return str(value)
128+
return None
129+
130+
131+
def _format_property(property: AXProperty) -> Union[str, None]:
132+
name = property.get("name")
133+
if name is None or (value_obj := property.get("value")) is None:
134+
return None
135+
value_type = value_obj["type"]
136+
value = value_obj["value"]
137+
value_formatted: Union[str, None] = None
138+
139+
if (value_formatted := _format_ax_value(value_type, value)) is not None:
140+
return f"{name}={value_formatted}"
141+
return None
142+
143+
144+
def _format_properties(node: AccessibilityNode) -> str:
145+
"""Formats the properties of a node into a simplified string representation."""
146+
included_properties: list[AXProperty] = [
147+
property
148+
for property in (node.get("properties") or [])
149+
if property["name"] in INCLUDED_NODE_PROPERTY_NAMES
150+
]
151+
152+
formatted = ", ".join(
153+
formatted
154+
for property in included_properties
155+
if (formatted := _format_property(property)) is not None
156+
)
157+
158+
if formatted:
159+
return f"({formatted})"
160+
return ""
161+
162+
98163
def format_simplified_tree(node: AccessibilityNode, level: int = 0) -> str:
99164
"""Formats a node and its children into a simplified string representation."""
100165
indent = " " * level
101166
name_part = f": {node.get('name')}" if node.get("name") else ""
102-
result = f"{indent}[{node.get('nodeId')}] {node.get('role')}{name_part}\n"
167+
value_part = f" value={node.get('value')}" if node.get("value") else ""
168+
properties_part = (
169+
f" {formatted_properties}"
170+
if (formatted_properties := _format_properties(node))
171+
else ""
172+
)
173+
result = (
174+
f"{indent}[{node.get('nodeId')}] {node.get('role')}{name_part}"
175+
f"{value_part}{properties_part}\n"
176+
)
103177

104178
children = node.get("children", [])
105179
if children:

tests/fixtures/ax_trees/input-range.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"3"
6262
],
6363
"backendDOMNodeId": 2,
64-
"frameId": "EB14D1D07B8B11BC38CA8A242657286A"
64+
"frameId": "4C8AFBFB04749B93F2BA71C9854C9B01"
6565
},
6666
{
6767
"nodeId": "3",
@@ -317,7 +317,7 @@
317317
},
318318
"value": {
319319
"type": "number",
320-
"value": 6
320+
"value": 5
321321
},
322322
"properties": [
323323
{

tests/fixtures/html_pages/input-range.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<section>
99
<h1>Range Input</h1>
1010
<div>
11-
<input type="range" id="volume" name="volume" min="0" max="11" />
11+
<input type="range" id="volume" name="volume" min="0" max="11" step="1" value="5" />
1212
<label for="volume">Volume</label>
1313
</div>
1414
</section>

tests/unit/a11y/test_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ async def test_select_tag_included_in_simplified(self, mock_stagehand_page: Stag
7979
[10] heading: Select Menus
8080
[11] LabelText
8181
[29] StaticText: Choose a pet:
82-
[12] select: Choose a pet:
82+
[12] select: Choose a pet: value=Hamster
8383
[15] MenuListPopup
8484
[19] option: Dog
8585
[22] option: Cat
86-
[25] option: Hamster
86+
[25] option: Hamster (selected=true)
8787
"""
8888
)
8989
assert actual["iframes"] == []
@@ -104,7 +104,7 @@ async def test_input_type_radio(self, mock_stagehand_page: StagehandPage, mock_s
104104
[10] group: Select a maintenance drone:
105105
[11] Legend
106106
[22] StaticText: Select a maintenance drone:
107-
[13] radio: Huey
107+
[13] radio: Huey (checked=true)
108108
[16] radio: Dewey
109109
[19] radio: Louie
110110
"""
@@ -125,7 +125,7 @@ async def test_input_type_range(self, mock_stagehand_page: StagehandPage, mock_s
125125
[8] generic
126126
[9] heading: Range Input
127127
[10] generic
128-
[11] slider: Volume
128+
[11] slider: Volume value=5 (valuemin=0, valuemax=11)
129129
[15] LabelText
130130
[17] StaticText: Volume
131131
"""

0 commit comments

Comments
 (0)