fix(folio): stop abusing contentDescription as data carrier (#45)

* feat(hierarchy): full-attribute selector system

- Add Attributes map to Element (raw platform attrs + serialized booleans)
- Add Selector / AttrFilter types for multi-filter AND matching
- Add matchAttr with alias expansion and substring/boolean semantics
- Add matchSelector (AND of all filters)
- id: and desc: keep exact/suffix/prefix semantics for backward compat
- text: widens to substring via matchAttr
- default: case routes unknown kinds to matchAttr (NEW)
- Add Tree.FindNode / Tree.FindAllNodes returning *Node
- Add Node.Find / Node.FindAll for scoped subtree string search
- Add Node.FindBySelector / Node.FindAllBySelector for object AND search
- Add attributeAliases for cross-platform name expansion

* test(hierarchy): full-attribute selector coverage

- raw resource-id: substring match
- label:/content-desc: alias expansion to accessibilityText on iOS
- scrollable:true/false boolean exact match
- title: iOS-only attribute, graceful nil on Android
- text: substring widening
- Selector AND: both filters must match; single miss returns nil
- Node.Find scoped search: descendants only, not siblings

* feat(verifier): object-form selectors + attrs + element-level find

- ax.find/findAll accept string or {attr:value} JS objects
- Object form builds Selector with AND semantics
- Returned element objects expose attrs sub-object (raw platform attrs)
- Returned element objects expose .find() and .findAll() scoped to subtree
- Element-level .find/.findAll accept string or object selectors

* feat(spec): extend AccessibilityElement and AccessibilityTree types

- AccessibilityElement gains attrs, find(), findAll()
- find/findAll on both Tree and Element accept string | AttrSelector
- AttrSelector = Record<string, string> for object-form AND matching

* feat(folio): migrate to chained object-form selectors

- Replace path queries (desc:X > desc:Y) with chained API
- Screen root lookups use { accessibilityText: "ScreenName" }
- Element-scoped searches use find/findAll with string or object
- Keep string selectors for descPrefix: and desc:Back (shows both forms)

* fix(folio): use account_card:id desc, expose balance via text semantics

* fix(folio): embed accountId in LedgerScreen desc, expose values via text semantics

* fix(spec): replace desc-parsing with text-based parseDollarCents extraction
This commit is contained in:
pj authored and GitHub committed 2026-04-25 23:43:19 +07:00
1 parent 776becdf4b
commit 34fb73d6cb
7 files changed
+593 -107

No files matched your search

@@ -123,7 +123,7 @@ private fun AccountCard(
.clip(RoundedCornerShape(RadiusLg))
.background(t.surface)
.border(1.dp, t.border, RoundedCornerShape(RadiusLg))
.semantics(mergeDescendants = true) { contentDescription = "account:$id:$balance" }
.semantics { contentDescription = "account_card:$id" }
.clickable(role = Role.Button, onClick = onClick)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -148,6 +148,11 @@ private fun AccountCard(
)
Text(txnLabel, style = Type.caption, color = t.textMuted)
}
Text(formatCents(balance), style = Type.bodyStrong, color = t.text)
Text(
formatCents(balance),
style = Type.bodyStrong,
color = t.text,
modifier = Modifier.semantics { contentDescription = "account_balance" },
)
}
}
@@ -75,7 +75,7 @@ fun LedgerScreen(accountId: String) {
val balance = balanceOf(txns)
Screen(
modifier = Modifier.testTag("LedgerScreen").semantics { contentDescription = "LedgerScreen" },
modifier = Modifier.testTag("LedgerScreen").semantics { contentDescription = "LedgerScreen:$accountId" },
header = {
Header(
title = account.name,
@@ -92,11 +92,14 @@ fun LedgerScreen(accountId: String) {
)
},
) {
Box(Modifier.size(1.dp).semantics { contentDescription = "active_account:$accountId" })
Box(Modifier.size(1.dp).semantics { contentDescription = "ledger_balance:$balance" })
Card {
Text("BALANCE", style = Type.label, color = t.textMuted)
Text(formatCents(balance), style = Type.balance, color = t.text)
Text(
formatCents(balance),
style = Type.balance,
color = t.text,
modifier = Modifier.semantics { contentDescription = "ledger_balance_display" },
)
}
Text(
"ACTIVITY",
@@ -138,7 +141,7 @@ private fun TxnRow(id: String, type: TxnType, amount: Long, note: String, date:
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 14.dp)
.semantics(mergeDescendants = true) { contentDescription = "ledger_row:$id:$signed" },
.semantics { contentDescription = "ledger_row:$id" },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
@@ -166,6 +169,7 @@ private fun TxnRow(id: String, type: TxnType, amount: Long, note: String, date:
formatCents(signed, signed = true),
style = Type.bodyStrong,
color = t.text,
modifier = Modifier.semantics { contentDescription = "txn_amount" },
)
}
}