|
5 | 5 | from pydantic import AnyUrl, BaseModel, Field, HttpUrl, create_model |
6 | 6 | from pydantic.fields import FieldInfo |
7 | 7 |
|
8 | | -from stagehand.types.a11y import AccessibilityNode |
| 8 | +from stagehand.types.a11y import AccessibilityNode, AXProperty, AXValue |
9 | 9 |
|
10 | 10 |
|
11 | 11 | def snake_to_camel(snake_str: str) -> str: |
@@ -95,11 +95,85 @@ def convert_dict_keys_to_snake_case(data: Any) -> Any: |
95 | 95 | return data |
96 | 96 |
|
97 | 97 |
|
| 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 | + |
98 | 163 | def format_simplified_tree(node: AccessibilityNode, level: int = 0) -> str: |
99 | 164 | """Formats a node and its children into a simplified string representation.""" |
100 | 165 | indent = " " * level |
101 | 166 | 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 | + ) |
103 | 177 |
|
104 | 178 | children = node.get("children", []) |
105 | 179 | if children: |
|
0 commit comments