test(sidecar): pin the tree-guarded keyboard dismissal

This commit is contained in:
pj committed 2026-08-15 21:08:03 +05:30
1 parent b4cd9d5cd1
commit 1e350f7fd3
1 file changed
+120
@@ -240,8 +240,128 @@ class InputTextTest {
} }
assertEquals(listOf("dumpsys input_method"), commands) assertEquals(listOf("dumpsys input_method"), commands)
} }
// Typing is not the only thing that raises the keyboard: tapping a field
// raises it too, and nothing was closing that one. The snapshot the picker
// chooses from is missing every app node the keyboard covers, so the step
// spends its budget choosing between the few targets left. Closing it
// before the tree is read is what gives the step its targets back.
@Test fun aKeyboardInTheTreeIsClosedBeforeTheTreeIsReturned() {
var backs = 0
val reads = mutableListOf<String>()
val settled = treeWithoutKeyboard(
IME_TREE,
IME_PACKAGE,
dismiss = { backs++ },
reread = { APP_TREE.also { reads.add(it) } },
sleep = {},
)
assertEquals(APP_TREE, settled)
assertEquals(1, backs, "one BACK closes the keyboard")
assertEquals(1, reads.size, "the tree is re-read once it is gone")
} }
// The guard has to be the tree itself. BACK with no keyboard open
// navigates out of the screen, so a snapshot that pressed it on every read
// would walk the fuzzer backwards out of the app a step at a time.
@Test fun aTreeWithNoKeyboardIsReturnedUntouched() {
var backs = 0
var reads = 0
val settled = treeWithoutKeyboard(
APP_TREE,
IME_PACKAGE,
dismiss = { backs++ },
reread = {
reads++
APP_TREE
},
sleep = {},
)
assertEquals(APP_TREE, settled)
assertEquals(0, backs, "no keyboard in the tree means no BACK")
assertEquals(0, reads, "and no second hierarchy read to pay for")
}
// An unknown IME package is the honest "cannot tell", and the safe way to
// be wrong is to leave the keyboard up rather than press BACK blind.
@Test fun anUnknownImePackageSendsNoBack() {
var backs = 0
assertEquals(
IME_TREE,
treeWithoutKeyboard(
IME_TREE,
null,
dismiss = { backs++ },
reread = { APP_TREE },
sleep = {},
),
)
assertEquals(0, backs)
}
// A keyboard the app puts straight back gets ONE back press, not one per
// re-read. The flag behind the older dismissal lags a BACK by up to 0.6s,
// and a burst of them inside that window is how a dismissal turns into
// navigation.
@Test fun aKeyboardThatStaysUpIsNotBackPressedRepeatedly() {
var backs = 0
var reads = 0
val settled = treeWithoutKeyboard(
IME_TREE,
IME_PACKAGE,
dismiss = { backs++ },
reread = {
reads++
IME_TREE
},
sleep = {},
)
assertEquals(IME_TREE, settled, "the caller still gets a tree")
assertEquals(1, backs)
assertTrue(
reads in 1..KEYBOARD_DISMISS_READS,
"bounded re-reads, got $reads",
)
}
@Test fun imePackageOfReadsTheComponentAndRejectsNonsense() {
assertEquals(
"com.google.android.inputmethod.latin",
imePackageOf(
"com.google.android.inputmethod.latin/.LatinIME\n",
),
)
assertEquals(null, imePackageOf("null"))
assertEquals(null, imePackageOf(""))
assertEquals(null, imePackageOf(" \n"))
}
@Test fun treeShowsImeMatchesTheImesOwnViewIdsOnly() {
assertTrue(treeShowsIme(IME_TREE, IME_PACKAGE))
assertTrue(!treeShowsIme(APP_TREE, IME_PACKAGE))
}
}
private const val IME_PACKAGE = "com.google.android.inputmethod.latin"
private val APP_TREE =
"""
{"attributes":{"resource-id":"AddAccountScreen"},"children":[
{"attributes":{"resource-id":"AccountNameField"},"children":[]},
{"attributes":{"resource-id":"AddAccountSubmit"},"children":[]}]}
""".trimIndent()
// The submit control is gone: an open keyboard does not merely cover the node,
// it takes it out of the tree the picker enumerates.
private val IME_TREE =
"""
{"attributes":{"resource-id":"AddAccountScreen"},"children":[
{"attributes":{"resource-id":"AccountNameField"},"children":[]},
{"attributes":{
"resource-id":"com.google.android.inputmethod.latin:id/keyboard_holder"
},"children":[]}]}
""".trimIndent()
private val IME_OPEN_DUMPSYS = private val IME_OPEN_DUMPSYS =
""" """
mCurMethodId=com.google.android.inputmethod.latin/.LatinIME mCurMethodId=com.google.android.inputmethod.latin/.LatinIME