Skip to content

[BUG] Android DeviceLab: lazy retry re-issues a tap across a navigation boundary, pressing the next screen's CTA #95

@laiskajoonas

Description

@laiskajoonas

Description

On the Android DeviceLab driver, the v1.1.16 lazy retry re-issues a tap across a navigation boundary, pressing a button on the next screen that the flow never asked to tap.

Scenario: a "Confirm" button triggers an async submit (network mutation) followed by navigation a few hundred ms later. The assert for the next screen starts probing immediately; its first probe fails (~500 ms in, the mutation is still in flight, the source screen is still up). maybeLazyRetryTap then evaluates its gates — tree hash unchanged since the tap, and the tapped "Confirm" still findable — and both pass, because the navigation simply hasn't happened yet. It re-issues the tap. The FindAndClick injection races the now-landing navigation and the tap fires on the new screen's bottom CTA at the same coordinates. The flow is then desynced (a screen got skipped) and dies later with a misleading "element not found".

Three contributing gaps, in order of importance:

  1. The decision is stale by injection time. The hash/findable gates are checked ~200 ms before FindAndClick actually injects; a navigation landing in that window turns the "retry" into a tap on a different screen. The gates need re-verification atomically with (or immediately before) injection.
  2. "Still findable" ignores enabled-state. In our app the Confirm button is disabled for the whole submit+navigate chain — a disabled source button is positive proof the tap landed, but the retry treats it as "tap had no effect". (The disabled re-render evidently doesn't change the tree hash either.)
  3. The step-level settle had already detected an effect. The original tapOn logged Tap caused UI change (attempt 1), yet the lazy retry later concluded the same tap "had no effect". When the tap step's own change-detector fired, the lazy retry should consider that tap effective and never re-issue it.

A per-step opt-out would also help (e.g. honouring retryTapIfNoChange: false in the lazy-retry path) — currently recordTap is called unconditionally in the tapOn path and maybeLazyRetryTap consults no tap options, so flows cannot exempt a submit-then-navigate button.

Steps to Reproduce

  1. RN app screen where a button triggers mutation → then → navigation (total ~0.5–2 s), keeping the button visible (and disabled) until navigation.
  2. Flow: tapOn: "Confirm"assertVisible: "<text on the next screen>". The next screen has its own bottom CTA at the same position as Confirm.
  3. Run with --driver devicelab under CI load. Intermittent: fails when the navigation lands inside the retry's decision→injection window.

Expected Behavior

A tap that demonstrably landed (step-level UI change detected; source button disabled) is never re-issued; a re-issued tap is never injected after the screen has changed.

Actual Behavior

From maestro-runner.log (two independent CI occurrences; timestamps from one):

11:19:00.556 FindAndClick hit for Confirm via ...textContains("Confirm").clickable(true): bounds=[63,2085][1017,2211] center=(540,2148)
11:19:01.059 Tap caused UI change (attempt 1)
11:19:01.059 Step 176 completed successfully (951ms): tapOn: text="Confirm"
11:19:01.060 Executing step 177: assertVisible: text="Just a few more steps!"
11:19:01.748 findElementFastWithLazyRetry first probe failed, calling maybeLazyRetryTap
11:19:01.766 lazy retry triggered: tap target Confirm still findable → tap had no effect
11:19:01.988 lazy retry: re-issued tap on Confirm after assertion probe failed

The re-issued tap landed on the next screen's CTA (same coordinates), advancing the app one screen further than the flow; the flow then failed ~12 s later on tapOn: id for a button that was no longer on screen. The hierarchy/screenshot artifacts confirm the app had been advanced past the screen the flow was driving. In the second occurrence the retry fired twice back-to-back.

Environment

  • OS: Ubuntu (GitHub Actions runner)
  • maestro-runner version: v1.1.16
  • Executor: --driver devicelab, --parallel 4
  • Device/Simulator: Android emulator, API 35, x86_64, 1080x2400
  • App: React Native / Expo, animations disabled for E2E

Flow File

- tapOn:
    id: "checkbox-termsOfService"
- tapOn:
    text: "Confirm"
    waitToSettleTimeoutMs: 200
- assertVisible: "Just a few more steps!"   # first probe triggers the lazy retry mid-navigation
- takeScreenshot: "account-opening_Start"
- tapOn:
    id: "account-opening-start-button"      # fails: the re-tap already pressed this screen's CTA

Related: found alongside a FindAndClick off-screen-bounds issue on the same suite (filed separately).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions