refactor(sidecar): drop iOS hierarchy helpers and their test

This commit is contained in:
pj committed 2026-06-08 19:35:28 +05:30
1 parent a9fdf49a62
commit 7c2f5e296c
2 files changed
-238

No files matched your search

@@ -772,115 +772,3 @@ private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? {
else -> null
}
}
// XCUIElementType raw values that accept a tap. The XCTest hierarchy carries
// no clickable flag, so tappability is derived from the element type that
// XCUITest itself derives from the accessibility traits.
internal val IOS_CLICKABLE_ELEMENT_TYPES = setOf(
9, // button
10, // radioButton
12, // checkBox
14, // popUpButton
15, // comboBox
16, // menuButton
17, // toolbarButton
20, // key
37, // segmentedControl
40, // switch
41, // toggle
42, // link
54, // menuItem
75, // cell
79, // stepper
80, // tab
)
// XCUIElementType raw values that accept text input.
internal val IOS_EDITABLE_ELEMENT_TYPES = setOf(
45, // searchField
49, // textField
50, // secureTextField
52, // textView
)
// XCUIElementType raw values that scroll.
internal val IOS_SCROLLABLE_ELEMENT_TYPES = setOf(
26, // table
32, // collectionView
46, // scrollView
58, // webView
)
// Checkable element types and the value convention ("1" when on) mirror the
// upstream content-descriptor mapping.
internal val IOS_CHECKABLE_ELEMENT_TYPES = setOf(
12, // checkBox
40, // switch
41, // toggle
)
private val IOS_ELEMENT_TYPE_NAMES = arrayOf(
"Any", "Other", "Application", "Group", "Window", "Sheet", "Drawer",
"Alert", "Dialog", "Button", "RadioButton", "RadioGroup", "CheckBox",
"DisclosureTriangle", "PopUpButton", "ComboBox", "MenuButton",
"ToolbarButton", "Popover", "Keyboard", "Key", "NavigationBar", "TabBar",
"TabGroup", "Toolbar", "StatusBar", "Table", "TableRow", "TableColumn",
"Outline", "OutlineRow", "Browser", "CollectionView", "Slider",
"PageIndicator", "ProgressIndicator", "ActivityIndicator",
"SegmentedControl", "Picker", "PickerWheel", "Switch", "Toggle", "Link",
"Image", "Icon", "SearchField", "ScrollView", "ScrollBar", "StaticText",
"TextField", "SecureTextField", "DatePicker", "TextView", "Menu",
"MenuItem", "MenuBar", "MenuBarItem", "Map", "WebView", "IncrementArrow",
"DecrementArrow", "Timeline", "RatingIndicator", "ValueIndicator",
"SplitGroup", "Splitter", "RelevanceIndicator", "ColorWell", "HelpTag",
"Matte", "DockItem", "Ruler", "RulerMarker", "Grid", "LevelIndicator",
"Cell", "LayoutArea", "LayoutItem", "Handle", "Stepper", "Tab",
"TouchBar", "StatusItem",
)
internal fun iosElementTypeName(elementType: Int): String =
IOS_ELEMENT_TYPE_NAMES.getOrElse(elementType) { "Other" }
// iosAxElementToTreeNode maps the raw XCTest accessibility tree to the
// TreeNode JSON shape the runner parses. The upstream content-descriptor
// mapping drops the element type, leaving no way to tell buttons and text
// fields apart, so fuzz verbs found no tap or typing candidates on iOS.
internal fun iosAxElementToTreeNode(element: hierarchy.AXElement): Map<String, Any?> {
val elementType = element.elementType
val title = element.title.orEmpty()
val value = element.value.orEmpty()
val label = element.label.orEmpty()
val editable = elementType in IOS_EDITABLE_ELEMENT_TYPES
val checked = elementType in IOS_CHECKABLE_ELEMENT_TYPES && value == "1"
// text carries the element's visible string, matching what Android
// surfaces in its text attribute. Editable fields expose only their
// typed content: their label is the field caption (e.g. "Email"), and
// leaking it into text would make an empty field look filled. Static
// text and buttons carry the visible string in the label.
val text = if (editable) value else title.ifEmpty { value }.ifEmpty { label }
val attributes = linkedMapOf(
"accessibilityText" to label,
"title" to title,
"value" to value,
"text" to text,
"hintText" to element.placeholderValue.orEmpty(),
"resource-id" to element.identifier.orEmpty(),
"bounds" to element.frame.boundsString,
"class" to iosElementTypeName(elementType),
"enabled" to element.enabled.toString(),
"focused" to element.hasFocus.toString(),
"selected" to element.selected.toString(),
"checked" to checked.toString(),
"scrollable" to (elementType in IOS_SCROLLABLE_ELEMENT_TYPES).toString(),
)
return linkedMapOf(
"attributes" to attributes,
"children" to element.children.map { iosAxElementToTreeNode(it) },
"clickable" to (elementType in IOS_CLICKABLE_ELEMENT_TYPES),
"enabled" to element.enabled,
"focused" to element.hasFocus,
"checked" to checked,
"selected" to element.selected,
"editable" to editable,
)
}
@@ -1,126 +0,0 @@
package dev.sanderling.sidecar
import hierarchy.AXElement
import hierarchy.AXFrame
import org.junit.Test
import kotlin.test.assertEquals
class IosHierarchyTest {
private fun element(
elementType: Int,
label: String = "",
identifier: String = "",
title: String = "",
value: String = "",
placeholderValue: String = "",
children: ArrayList<AXElement> = arrayListOf(),
) = AXElement(
label,
elementType,
identifier,
0,
0L,
0,
false,
0,
false,
placeholderValue,
value,
AXFrame(10f, 20f, 100f, 50f),
true,
title,
children,
)
@Test
fun buttonIsClickableNotEditable() {
val node = iosAxElementToTreeNode(element(elementType = 9, label = "Sign in", identifier = "LoginSubmit"))
assertEquals(true, node["clickable"])
assertEquals(false, node["editable"])
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("Button", attributes["class"])
assertEquals("LoginSubmit", attributes["resource-id"])
assertEquals("Sign in", attributes["accessibilityText"])
}
@Test
fun textFieldIsEditableNotClickable() {
val node = iosAxElementToTreeNode(element(elementType = 49, value = "[email protected]"))
assertEquals(false, node["clickable"])
assertEquals(true, node["editable"])
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("TextField", attributes["class"])
assertEquals("[email protected]", attributes["text"])
assertEquals("[email protected]", attributes["value"])
}
@Test
fun titleWinsOverValueForText() {
val node = iosAxElementToTreeNode(element(elementType = 48, title = "Heading", value = "ignored"))
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("Heading", attributes["text"])
assertEquals("StaticText", attributes["class"])
}
@Test
fun staticTextFallsBackToLabelForText() {
val node = iosAxElementToTreeNode(element(elementType = 48, label = "$1,234.00"))
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("$1,234.00", attributes["text"])
}
@Test
fun buttonFallsBackToLabelForText() {
val node = iosAxElementToTreeNode(element(elementType = 9, label = "Sign in"))
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("Sign in", attributes["text"])
}
@Test
fun emptyTextFieldDoesNotLeakLabelIntoText() {
val node = iosAxElementToTreeNode(element(elementType = 49, label = "Email"))
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("", attributes["text"])
assertEquals("Email", attributes["accessibilityText"])
}
@Test
fun switchOnIsChecked() {
val node = iosAxElementToTreeNode(element(elementType = 40, value = "1"))
assertEquals(true, node["checked"])
assertEquals(true, node["clickable"])
}
@Test
fun scrollViewIsScrollable() {
val node = iosAxElementToTreeNode(element(elementType = 46))
@Suppress("UNCHECKED_CAST")
val attributes = node["attributes"] as Map<String, String>
assertEquals("true", attributes["scrollable"])
assertEquals("ScrollView", attributes["class"])
}
@Test
fun childrenAreMappedRecursively() {
val child = element(elementType = 48, title = "Inner")
val node = iosAxElementToTreeNode(element(elementType = 1, children = arrayListOf(child)))
@Suppress("UNCHECKED_CAST")
val children = node["children"] as List<Map<String, Any?>>
assertEquals(1, children.size)
@Suppress("UNCHECKED_CAST")
val childAttributes = children[0]["attributes"] as Map<String, String>
assertEquals("Inner", childAttributes["text"])
}
@Test
fun unknownElementTypeFallsBackToOther() {
assertEquals("Other", iosElementTypeName(999))
}
}