fix(runner): gate first observe on the app window being drawn, not just resumed

This commit is contained in:
pj committed 2026-05-31 14:33:09 +05:30
1 parent 3fe92537be
commit 06213e444e
1 file changed
+37 -7
+37 -7
View File
@@ -290,15 +290,22 @@ func ensureForeground(ctx context.Context, options Options, logger *slog.Logger,
// never hang the run. // never hang the run.
const foregroundReadyAttempts = 8 const foregroundReadyAttempts = 8
// waitForForeground blocks until the app under test is actually on top, so the // waitForForeground blocks until the app under test is actually on screen, so
// first action never lands on a leftover screen or a freshly-booted device's // the first observe never captures a leftover screen or a freshly-booted
// system dialog (e.g. Android's "set a screen lock" prompt). Drivers without // device's system dialog (e.g. Android's "set a screen lock" prompt). Drivers
// ForegroundChecker (web, iOS) and an unknown foreground both skip the gate. // without ForegroundChecker (web) and an unknown foreground both skip the gate.
//
// It is not enough that the app is the resumed activity: ResumedActivity flips
// to a freshly launched app ~before its first frame draws, so gating on it
// alone lets the first observe read the outgoing app. When the driver can also
// report the focused window, the gate additionally waits for that window to
// name the app, which only happens once it is genuinely drawn.
func waitForForeground(ctx context.Context, options Options, logger *slog.Logger) { func waitForForeground(ctx context.Context, options Options, logger *slog.Logger) {
checker, ok := options.Driver.(driver.ForegroundChecker) checker, ok := options.Driver.(driver.ForegroundChecker)
if !ok || options.BundleID == "" { if !ok || options.BundleID == "" {
return return
} }
focusChecker, hasFocus := options.Driver.(driver.FocusedWindowChecker)
for attempt := range foregroundReadyAttempts { for attempt := range foregroundReadyAttempts {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return return
@@ -308,12 +315,29 @@ func waitForForeground(ctx context.Context, options Options, logger *slog.Logger
logger.Warn("foreground check failed before first step", "err", err) logger.Warn("foreground check failed before first step", "err", err)
return return
} }
if foreground == "" || foreground == options.BundleID { if foreground == "" {
return return // foreground unknowable (e.g. iOS); don't block the run
} }
if foreground != options.BundleID {
logger.Warn("app not in foreground at start; bringing it forward", logger.Warn("app not in foreground at start; bringing it forward",
"foreground", foreground, "want", options.BundleID, "attempt", attempt) "foreground", foreground, "want", options.BundleID, "attempt", attempt)
bringToForeground(ctx, options, logger, 0) bringToForeground(ctx, options, logger, 0)
continue
}
if !hasFocus {
return // resumed is the app and no finer signal exists
}
focused, err := focusChecker.FocusedWindowApp(ctx)
if err != nil {
logger.Warn("focus check failed before first step", "err", err)
return
}
if focused == options.BundleID {
return // window is drawn; safe to observe
}
logger.Warn("app resumed but window not yet drawn; waiting",
"focused", focused, "want", options.BundleID, "attempt", attempt)
settleForForeground(ctx, options)
} }
logger.Warn("app never reached foreground before first step; proceeding anyway", logger.Warn("app never reached foreground before first step; proceeding anyway",
"want", options.BundleID) "want", options.BundleID)
@@ -331,10 +355,16 @@ func bringToForeground(ctx context.Context, options Options, logger *slog.Logger
logger.Warn("relaunch failed", "step", stepIndex, "err", err) logger.Warn("relaunch failed", "step", stepIndex, "err", err)
return false return false
} }
settleForForeground(ctx, options)
return true
}
// settleForForeground waits one idle window for the UI to settle, bounding the
// wait by the driver's idle timeout.
func settleForForeground(ctx context.Context, options Options) {
idleCtx, cancel := context.WithTimeout(ctx, options.IdleTimeout) idleCtx, cancel := context.WithTimeout(ctx, options.IdleTimeout)
_ = options.Driver.WaitForIdle(idleCtx, options.IdleTimeout) _ = options.Driver.WaitForIdle(idleCtx, options.IdleTimeout)
cancel() cancel()
return true
} }
func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.Action, tree *hierarchy.Tree) error { func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.Action, tree *hierarchy.Tree) error {