Skip to content

[BUG] Web | tapOn text+index does not span iframes — silently re-taps top-frame match (#67 fix incomplete) #72

@richjun

Description

@richjun

Description

Follow-up to #67 (closed, marked fixed in v1.1.13 by commits 8ecbbc6 / 4ca8fd8 / ed90309).

tapOn with text + index no longer drops the index field with the old "index" is not supported on web — will be ignored warning — that part of #67 is fixed. But index does not enumerate matches across iframes: when the Nth match would be inside an iframe, the runner silently re-taps the last in-range top-frame match instead, reports tapOn ✓, and never raises an error. The result is a false-positive that's hard to detect — tapOn "succeeded" but the click landed on the wrong button.

Same yaml on upstream Maestro 2.5.1 taps the iframe-hosted match.

Environment

  • maestro-runner: 1.1.13 (commit 7addd21, built 2026-05-05T14:07:52Z, go1.26.0)
  • OS: macOS 15 (Darwin 25.2.0, arm64)
  • Executor: web (Chrome stable, headed via CDP)
  • Comparison: Maestro (mobile-dev-inc) 2.5.1 — equivalent flow resolves the iframe-hosted match.

One-line repro

A single self-contained HTML page is hosted on Cloudflare R2 — no app login, no third-party site, no local server. The page has two Help buttons: one in the top frame, one inside an iframe (via srcdoc). Each button's click handler appends a uniquely-labelled marker (TOP_HELP_CLICKED / IFRAME_HELP_CLICKED) to the top frame so the test can assert which one actually received the click.

# repro.yaml
url: "https://pub-4e93ec0f0add49d78a646e058039eb5b.r2.dev/helpidx.html"
name: help-index-iframe-repro
---
- launchApp
- assertVisible: "Help"
- tapOn:
    text: "Help"
    index: 1                              # SHOULD tap the iframe Help (2nd in document order)
- extendedWaitUntil:
    visible: "IFRAME_HELP_CLICKED"        # FAILS — iframe button never received the click
    timeout: 3000
maestro-runner --platform web test repro.yaml

Result matrix

Three cases were run against the same R2-hosted page on maestro-runner 1.1.13:

# tapOn post-tap assertion maestro result what actually happened
A text: "Help", index: 0 extendedWaitUntil visible "TOP_HELP_CLICKED" ✓ PASS top-frame Help was tapped — correct
B text: "Help", index: 1 extendedWaitUntil visible "IFRAME_HELP_CLICKED" ✗ FAIL (3s timeout) iframe Help was not tapped
C text: "Help", index: 1 extendedWaitUntil visible "TOP_HELP_CLICKED" ✓ PASS top-frame Help was tapped again (the same one as case A)

Case C is the smoking gun: with only one top-frame Help button, index: 1 should resolve to the iframe-hosted Help (or fail with not-found). Instead it silently re-resolves to the top-frame Help — same element as index: 0. Maestro reports tapOn ✓ either way.

Verbatim run output

Case A (index: 0):

✓ launchApp                                            (527ms)
✓ assertVisible: text="Help"                           (5ms)
✓ tapOn: text="Help"                                   (54ms)
✓ extendedWaitUntil: visible text="TOP_HELP_CLICKED"   (0ms)
help-index-0-r2    ✓ PASS    4 steps    4 pass    0 fail    0 skip    595ms

Case B (index: 1 + assert iframe marker):

✓ launchApp                                                (530ms)
✓ assertVisible: text="Help"                               (3ms)
✓ tapOn: text="Help"                                       (44ms)   ← reports success
✗ extendedWaitUntil: visible text="IFRAME_HELP_CLICKED"    (3.0s)   ← but iframe button was never clicked
  ╰─ Wait condition not met within 3s
help-index-1-r2    ✗ FAIL    4 steps    3 pass    1 fail    0 skip    3.8s

Case C (index: 1 + assert top marker — proves the silent fall-back):

✓ launchApp                                            (528ms)
✓ assertVisible: text="Help"                           (4ms)
✓ tapOn: text="Help"                                   (43ms)
✓ extendedWaitUntil: visible text="TOP_HELP_CLICKED"   (1ms)        ← top button hit again
help-index-1-r2-checktop    ✓ PASS    5 steps    5 pass    0 fail    0 skip    742ms

A screenshot taken in Case C confirms only TOP_HELP_CLICKED is rendered after the index: 1 tap — IFRAME_HELP_CLICKED is absent and the iframe Help button is visibly untouched.

Repro page source — helpidx.html (single file, ~1.8 KB)

The hosted page is the self-contained file below. Copy into helpidx.html and serve from anywhere if you'd rather not hit R2.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>maestro-runner Help text+index test</title>
  <style>
    body { font-family: sans-serif; margin: 24px; background: #fff; color: #222; }
    button { padding: 8px 16px; margin: 4px; cursor: pointer; }
    iframe { width: 600px; height: 220px; border: 1px solid #999; display: block; margin-top: 12px; }
    .marker {
      margin-top: 16px; padding: 8px 12px;
      background: #fff3cd; border: 1px solid #ffeeba;
      border-radius: 4px; display: inline-block;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <h1>Help index test</h1>
  <p>
    Top-frame <code>Help</code> button is the first occurrence in document order.
    The iframe below (via <code>srcdoc</code>) contains a second <code>Help</code> button.
    Whichever button receives the click appends a uniquely-labelled
    <code>&lt;div class="marker"&gt;…&lt;/div&gt;</code> to the top frame so the test can assert
    which one was actually hit.
  </p>

  <button id="top-help">Help</button>

  <iframe id="inner" title="inner iframe with second Help" srcdoc='
<!DOCTYPE html>
<html lang="en">
<body style="font-family:sans-serif;margin:16px">
  <button id="iframe-help">Help</button>
  <p>Help button inside iframe (second occurrence in document order).</p>
  <script>
    document.getElementById("iframe-help").addEventListener("click", () => {
      parent.document.body.insertAdjacentHTML(
        "beforeend",
        "<div class=\"marker\">IFRAME_HELP_CLICKED</div>"
      );
    });
  </script>
</body>
</html>
  '></iframe>

  <script>
    document.getElementById("top-help").addEventListener("click", () => {
      document.body.insertAdjacentHTML(
        "beforeend",
        "<div class=\"marker\">TOP_HELP_CLICKED</div>"
      );
    });
  </script>
</body>
</html>

Expected

tapOn { text: "Help", index: N } enumerates Help matches in document order across the top frame and every same-origin iframe (consistent with how 8b33eb2 already walks iframes for "find one match"), and taps the Nth match. If N is out of range, surface a clear not found error rather than silently re-tapping a top-frame element. This matches upstream Maestro 2.5.1.

Likely root cause

The index selector resolution path in pkg/driver/browser/cdp/finder.go (added in 8ecbbc6 / 4ca8fd8) appears to enumerate matches only within the top frame when text is combined with index. The same-origin iframe walk added in 8b33eb2 (_collectDocs() / _findMatchingElements) is not threaded through the text+index code path, so iframe matches are never visible to the index slot. When index >= len(top-frame matches), the resolver clamps or falls back to the last in-range top-frame match instead of erroring.

Suggested fix

In the text + index branch, build the candidate list from the same _collectDocs()-driven scan that the rest of the iframe-aware finders use, sort by document-order across (top, iframe-1, iframe-2, …), index into the unified list, and on out-of-range raise a text 'X' index N: only K match(es) found error rather than tapping the closest in-range element. Add a peer test to pkg/driver/browser/cdp/finder_iframe_test.go covering the index: 1 → iframe-hosted match case.

Happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions