fix(sample-app): address PR #18 review findings (#19)

- WebLedgerStore.accountExistsByName case-insensitive (matches SQL COLLATE NOCASE)
- drop SampleApplication.maybeInjectDebugError; sample spec now passes clean
- drop noLogcatErrors from sample spec (default matches system-wide E logs)
- openRandomAccount picks uniformly from findAll instead of first match
- clear txnError on any add-transaction interaction, not only valid amount input
- escapeForAdbInputText quotes shell metacharacters (quotes, backslash, etc.)
- extract buildClearKeyevents helper with empty/normal/cap tests
- broaden spec_integration_test.go to cover home, add-account, ledger,
  add-transaction action generators
- document FocusTracker single-focus invariant
This commit is contained in:
pj authored and GitHub committed 2026-04-20 13:31:50 +07:00
1 parent 8381a98aaf
commit c1de4bf57a
8 files changed
+284 -26

No files matched your search

@@ -93,6 +93,32 @@ class StubDriverBackend(private val platform: String) : DriverBackend {
// hierarchy that reports an enormous text length for the focused field.
internal const val MAX_CLEAR_DELETES: Int = 1024
internal fun buildClearKeyevents(textLength: Int): List<String> {
if (textLength <= 0) return emptyList()
val deletes = minOf(textLength, MAX_CLEAR_DELETES)
val args = mutableListOf("shell", "input", "keyevent", "KEYCODE_MOVE_END")
repeat(deletes) { args.add("KEYCODE_DEL") }
return args
}
// `adb shell input text` runs through a remote sh, so shell metacharacters
// in the payload would be interpreted by the device shell. Substitute
// spaces with %s (input's escape) and backslash-escape characters sh
// would otherwise expand. Keep this list conservative; anything not
// listed passes through literally.
internal fun escapeForAdbInputText(text: String): String {
val sb = StringBuilder(text.length)
for (ch in text) {
when (ch) {
' ' -> sb.append("%s")
'\\', '"', '\'', '&', '|', ';', '<', '>', '(', ')', '*', '?',
'$', '`', '[', ']', '{', '}', '~', '#', -> sb.append('\\').append(ch)
else -> sb.append(ch)
}
}
return sb.toString()
}
// Matches a uiautomator-dump <node ...> tag where `focused="true"` is
// present. Captures only the tag's attribute string so we can pull
// `text="..."` out of it without building a full XML tree.
@@ -188,16 +214,13 @@ class StubDriverBackend(private val platform: String) : DriverBackend {
// pure replace: read the current value's length from the hierarchy,
// then move-end + N backspaces before typing.
clearFocusedField()
runAdb(listOf("shell", "input", "text", text.replace(" ", "%s")))
runAdb(listOf("shell", "input", "text", escapeForAdbInputText(text)))
}
private fun clearFocusedField() {
val current = focusedFieldText() ?: return
if (current.isEmpty()) return
val deletes = minOf(current.length, MAX_CLEAR_DELETES)
val keyevents = mutableListOf("shell", "input", "keyevent", "KEYCODE_MOVE_END")
repeat(deletes) { keyevents.add("KEYCODE_DEL") }
runAdb(keyevents)
runAdb(buildClearKeyevents(current.length))
}
private fun focusedFieldText(): String? {
@@ -3,9 +3,51 @@ package dev.uatu.sidecar
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class InputTextClearTest {
@Test fun buildClearKeyeventsEmptyWhenNothingToDelete() {
assertEquals(emptyList(), StubDriverBackend.buildClearKeyevents(0))
assertEquals(emptyList(), StubDriverBackend.buildClearKeyevents(-3))
}
@Test fun buildClearKeyeventsPrefixesMoveEndThenOneDelPerChar() {
val args = StubDriverBackend.buildClearKeyevents(3)
assertEquals(listOf("shell", "input", "keyevent", "KEYCODE_MOVE_END",
"KEYCODE_DEL", "KEYCODE_DEL", "KEYCODE_DEL"), args)
}
@Test fun buildClearKeyeventsCapsAtMaxClearDeletes() {
val huge = StubDriverBackend.MAX_CLEAR_DELETES * 10
val args = StubDriverBackend.buildClearKeyevents(huge)
val deletes = args.count { it == "KEYCODE_DEL" }
assertEquals(StubDriverBackend.MAX_CLEAR_DELETES, deletes)
assertEquals("KEYCODE_MOVE_END", args[3])
}
@Test fun escapeForAdbInputTextSubstitutesSpaces() {
assertEquals("hello%sworld", StubDriverBackend.escapeForAdbInputText("hello world"))
}
@Test fun escapeForAdbInputTextEscapesShellMetacharacters() {
val escaped = StubDriverBackend.escapeForAdbInputText("a&b|c;d\$e`f")
assertEquals("a\\&b\\|c\\;d\\\$e\\`f", escaped)
}
@Test fun escapeForAdbInputTextEscapesQuotesAndBackslash() {
assertEquals("\\'", StubDriverBackend.escapeForAdbInputText("'"))
assertEquals("\\\"", StubDriverBackend.escapeForAdbInputText("\""))
assertEquals("\\\\", StubDriverBackend.escapeForAdbInputText("\\"))
}
@Test fun escapeForAdbInputTextLeavesSimpleTextAlone() {
assertEquals("12.34", StubDriverBackend.escapeForAdbInputText("12.34"))
assertEquals("Coffee", StubDriverBackend.escapeForAdbInputText("Coffee"))
assertTrue("-5" == StubDriverBackend.escapeForAdbInputText("-5"))
}
@Test fun parsesTextFromFocusedNode() {
val xml = """
<hierarchy>