From 2e817e3167881c131d01970db20d226d402c9cb2 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 7 Jun 2026 00:28:40 +0530 Subject: [PATCH] fix(ioscompanion): wait out unresolved accessibility values after launch --- internal/driver/ioscompanion/driver.go | 28 ++++++++++++++++++-- internal/driver/ioscompanion/hierarchymap.go | 16 +++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/driver/ioscompanion/driver.go b/internal/driver/ioscompanion/driver.go index 321869f..132db65 100644 --- a/internal/driver/ioscompanion/driver.go +++ b/internal/driver/ioscompanion/driver.go @@ -298,6 +298,8 @@ func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, e return fmt.Errorf("launch %s: %w", d.bundleID, err) } + d.waitForAccessibilityReady(ctx) + if !d.pastePrimed { if err := warmUpPaste(ctx, d.makeRunner()); err != nil { fmt.Fprintf(d.output, "paste warm-up failed (continuing): %v\n", err) @@ -307,6 +309,28 @@ func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, e return nil } +// accessibilityReadyTimeout bounds how long Launch waits for the freshly +// started app's accessibility tree to stop reporting placeholder values. +const accessibilityReadyTimeout = 10 * time.Second + +// waitForAccessibilityReady polls the accessibility dump until no element +// value reads as the bridge's unresolved-value sentinel. During app cold start +// the bridge reports "Invalid" for values it cannot resolve yet; a snapshot +// taken in that window shows phantom field content. Launch owns this wait so +// the first post-launch snapshot is trustworthy; the per-step settle cap is +// too short to absorb a cold start. +func (d *Driver) waitForAccessibilityReady(ctx context.Context) { + deadline := time.Now().Add(accessibilityReadyTimeout) + for time.Now().Before(deadline) && ctx.Err() == nil { + dump, err := d.describeAll(ctx) + if err == nil && !hasUnresolvedValues(dump) { + return + } + time.Sleep(StabilityPollInterval) + } + fmt.Fprintln(d.output, "accessibility tree still reports unresolved values after launch; first snapshot may be unreliable") +} + // clearAppState resets the app to a first-launch state. With an app path it // uninstalls and reinstalls; without one it falls back to wiping the app's data // container and warns once that a full reinstall needs the app path. @@ -455,11 +479,11 @@ func (d *Driver) resolveSelectorCenter(ctx context.Context, selector string) (in } func (d *Driver) InputText(ctx context.Context, text string) error { - // The field target is only needed for the pasteboard fallback. Resolving it + // The field target is only needed for the pasteboard path. Resolving it // requires a describe-all, so the fast keyboard path skips that round-trip // and lets inputText send the key presses directly. var field fieldTarget - if _, skipped := typeString(text); len(skipped) > 0 { + if usesPasteboard(text) { field = d.resolveInputField(ctx) } return inputText(ctx, d.makeRunner(), text, field) diff --git a/internal/driver/ioscompanion/hierarchymap.go b/internal/driver/ioscompanion/hierarchymap.go index 1aef8fa..fa8a227 100644 --- a/internal/driver/ioscompanion/hierarchymap.go +++ b/internal/driver/ioscompanion/hierarchymap.go @@ -17,6 +17,22 @@ type rawFrame struct { // rawElement is one entry in the flat describe-all dump returned by the // simulator companion. Only the fields the mapper consumes are declared; // unknown fields are ignored. +// unresolvedValueSentinel is what the accessibility bridge reports for a value +// it cannot resolve yet (typically during app cold start). It is bridge state, +// not app content. +const unresolvedValueSentinel = "Invalid" + +// hasUnresolvedValues reports whether any element in the flat dump still +// carries the bridge's unresolved-value sentinel. +func hasUnresolvedValues(dump []byte) bool { + for _, element := range decodeDump(dump) { + if stringValue(element.AXValue) == unresolvedValueSentinel { + return true + } + } + return false +} + type rawElement struct { Frame rawFrame `json:"frame"` AXUniqueID *string `json:"AXUniqueId"`