From a745ec79387e443958c8f1e9efc6ca2f18666fbe Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 7 Jun 2026 11:46:09 +0530 Subject: [PATCH] fix(ioscompanion): retry describe past transient collapsed accessibility dumps --- internal/driver/ioscompanion/driver.go | 49 ++++++++++++++++---- internal/driver/ioscompanion/hierarchymap.go | 14 ++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/internal/driver/ioscompanion/driver.go b/internal/driver/ioscompanion/driver.go index 57b0e1f..c61e84c 100644 --- a/internal/driver/ioscompanion/driver.go +++ b/internal/driver/ioscompanion/driver.go @@ -660,18 +660,47 @@ func (d *Driver) ForegroundApp(ctx context.Context) (string, error) { return other, nil } -// describeAll fetches the flat accessibility dump with one-restart recovery. +// collapsedDumpRetries and collapsedDumpDelay bound how long describeAll waits +// out a collapsed accessibility dump. The bridge briefly reports only the app +// shell (no UI content) during cold start and screen transitions; it recovers +// within a few hundred milliseconds. Re-fetching past the collapse keeps the +// runner from acting on, and snapshotting, an empty tree. +const collapsedDumpRetries = 6 +const collapsedDumpDelay = 150 * time.Millisecond + +// describeAll fetches the flat accessibility dump with one-restart recovery, +// retrying past a transient collapsed dump so callers see real UI content. func (d *Driver) describeAll(ctx context.Context) ([]byte, error) { - var dump []byte - err := d.withRecovery(ctx, func() error { - info, infoErr := d.companion.AccessibilityInfo(ctx) - if infoErr != nil { - return infoErr + fetch := func() ([]byte, error) { + var dump []byte + err := d.withRecovery(ctx, func() error { + info, infoErr := d.companion.AccessibilityInfo(ctx) + if infoErr != nil { + return infoErr + } + dump = []byte(info) + return nil + }) + return dump, err + } + + dump, err := fetch() + if err != nil { + return dump, err + } + for attempt := 0; attempt < collapsedDumpRetries && dumpIsCollapsed(dump); attempt++ { + select { + case <-ctx.Done(): + return dump, nil + case <-time.After(collapsedDumpDelay): } - dump = []byte(info) - return nil - }) - return dump, err + next, nextErr := fetch() + if nextErr != nil { + return dump, nil + } + dump = next + } + return dump, nil } // makeRunner builds the input runner backed by the current transport. The text diff --git a/internal/driver/ioscompanion/hierarchymap.go b/internal/driver/ioscompanion/hierarchymap.go index c87a822..8b0db9d 100644 --- a/internal/driver/ioscompanion/hierarchymap.go +++ b/internal/driver/ioscompanion/hierarchymap.go @@ -19,6 +19,20 @@ type rawFrame struct { // it maps to an empty value rather than surfacing as literal field text. const emptyFieldValueSentinel = "Invalid" +// dumpIsCollapsed reports whether a flat describe-all dump carries no real UI +// content: it is empty or holds only the application shell. The accessibility +// bridge briefly returns this state during cold start and screen transitions +// before the real tree reappears. +func dumpIsCollapsed(dump []byte) bool { + elements := decodeDump(dump) + for _, element := range elements { + if element.Type != "" && element.Type != "Application" { + return false + } + } + return true +} + // 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.