mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(runner): confirm focus moved before typing
InputText tapped its target, slept, then typed. Android and web both inject into whatever holds focus, so a tap that missed sent the whole string somewhere else and nothing reported it. On an emulator with a floating keyboard panel parked over the password field, the tap pressed the keyboard's emoji key and every step appended the password to the email instead, forever, because the setup leaf is guarded on the password being empty. The hierarchy is re-read after the tap and the target, or something in its subtree, must hold focus. Platforms whose hierarchy carries no focused attribute skip the read, so they pay nothing. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
6fd3fef9fa
commit
9a2bbbf769
2 files changed
+188
No files matched your search
@@ -653,6 +653,9 @@ func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.A
|
||||
return "", ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
if err := confirmFocus(ctx, drv, action.On, tree); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// InputText replaces the field's content: erase what the target
|
||||
// holds before typing. Appending instead lets repeated draws grow
|
||||
@@ -724,6 +727,94 @@ func inputReplacesText(drv driver.DeviceDriver) bool {
|
||||
return ok && replacer.ReplacesTextOnInput()
|
||||
}
|
||||
|
||||
// confirmFocus fails the action when the device reports focus on an element
|
||||
// other than the one the focus tap aimed at. Typing is a blind write to
|
||||
// whatever holds focus, so a tap the target never received (a keyboard overlay
|
||||
// window covering it, a target that cannot take focus) would stream the
|
||||
// characters into a different field, corrupting it and every property that
|
||||
// reads it. Hierarchies that carry no focus at all (iOS) leave nothing to
|
||||
// compare against, so those platforms are not charged the extra read.
|
||||
func confirmFocus(
|
||||
ctx context.Context,
|
||||
drv driver.DeviceDriver,
|
||||
selector string,
|
||||
tree *hierarchy.Tree,
|
||||
) error {
|
||||
if selector == "" || !reportsFocus(tree) {
|
||||
return nil
|
||||
}
|
||||
dump, err := drv.Hierarchy(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("focus check for %s: %w", selector, err)
|
||||
}
|
||||
current, err := hierarchy.Parse(dump)
|
||||
if err != nil {
|
||||
return fmt.Errorf("focus check for %s: %w", selector, err)
|
||||
}
|
||||
focused := focusedElement(current)
|
||||
if focused == nil {
|
||||
return nil
|
||||
}
|
||||
if target := current.FindNode(selector); target != nil && holdsFocus(target) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"focus tap on %s did not focus it: %s holds focus, so the text would land there",
|
||||
selector, elementName(focused),
|
||||
)
|
||||
}
|
||||
|
||||
// reportsFocus reports whether the platform describes focus at all, which is
|
||||
// what makes a post-tap focus check meaningful.
|
||||
func reportsFocus(tree *hierarchy.Tree) bool {
|
||||
if tree == nil {
|
||||
return false
|
||||
}
|
||||
for _, element := range tree.Elements {
|
||||
if _, ok := element.Attributes["focused"]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func focusedElement(tree *hierarchy.Tree) *hierarchy.Element {
|
||||
for _, element := range tree.Elements {
|
||||
if element.Focused {
|
||||
return element
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// holdsFocus accepts focus anywhere in the target's subtree: a selector often
|
||||
// names the field wrapper while the platform reports focus on the inner
|
||||
// editable node.
|
||||
func holdsFocus(node *hierarchy.Node) bool {
|
||||
if node.Focused {
|
||||
return true
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
if holdsFocus(child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func elementName(element *hierarchy.Element) string {
|
||||
switch {
|
||||
case element.ResourceID != "":
|
||||
return element.ResourceID
|
||||
case element.Description != "":
|
||||
return element.Description
|
||||
case element.Class != "":
|
||||
return element.Class
|
||||
default:
|
||||
return "an unnamed element"
|
||||
}
|
||||
}
|
||||
|
||||
// existingTextLength returns the character count of the InputText target's
|
||||
// current text, so the runner can erase it before typing. Zero when the
|
||||
// target cannot be resolved or holds no text.
|
||||
|
||||
Reference in new issue
Block a user