Repository navigation
tap has no occlusion/hit-test guard — can succeed against overlay-covered widgets #265
Description
Tracked internally as FP-10 (https://alphawavesystems.atlassian.net/browse/FP-10).
Problem
_tap/_doubleTap/_longPress in probe_agent/lib/src/executor.dart (~lines 311-402) resolve a selector to an Element via ProbeFinder (which already filters to mounted + current-route + non-zero-size elements — the separate off-screen/scroll-into-view gap is tracked as PT-03 in IMPROVEMENT_TASKS.md) and then interact with it one of two ways:
- If
element.widget is Semantics,_tryDirectTapwalks down to aGestureDetector/InkResponsedescendant and callswidget.onTap!()directly — no hit-testing at all. - Otherwise (or if
_tryDirectTapfinds nothing), a synthetic pointer gesture fires at the element'slocalToGlobalcenter via_createGesture.
Neither path checks whether that screen coordinate is actually the topmost hit. So tap can succeed against a button that's mounted, on the current route, and non-zero-size, but visually covered by something a real user's finger would hit first — a modal barrier, a blocking/loading overlay, a snackbar, a bottom sheet, another Stack sibling drawn on top. The test reports success even though a human tester could not have produced that outcome.
Correction to an earlier framing
An earlier proposal was to reorder tap dispatch (hit-tested gesture first, direct-invoke as fallback). That's wrong — see PT-04/PT-05 in IMPROVEMENT_TASKS.md (confirmed: a real hit-tested tap on a Semantics-wrapped widget didn't reliably focus text fields or invoke onTap on buttons/drawer-triggers; _tryDirectTap exists specifically to fix that) and PT-22 (closed — a crash that looked like evidence against the direct-invoke path turned out to be an unrelated, rare Flutter framework race, irrelevant in release builds). No evidence in this repo's own history supports reordering; doing so would reintroduce PT-04/PT-05.
Correct scope
Add an occlusion guard that runs before either tap path fires, independent of invocation order: hit-test the target's screen-space center (e.g. via the current binding's hitTestInView) and confirm the resolved element/RenderObject is actually part of the hit-test result at that point. If occluded, tap should fail loudly (e.g. "target is covered by <widget>, not tappable") instead of silently proceeding via _tryDirectTap's bypass.
Suggested implementation
- New helper, e.g.
_isOccluded(Element element, Offset point), inexecutor.dartorfinder.dart(alongside_isVisible). - Called from
_tap,_doubleTap,_longPressbefore either invocation path. - Open question: hard-fail vs. warn-and-proceed — given PT-02's theme (executor silently no-ops on malformed input) and this project's general direction toward loud failures, hard-fail seems consistent, but needs a maintainer call.
- Test plan: Dart widget test in
probe_agent/test/— a button under aStackcovered by an opaque sibling /ModalBarrier, asserting tap fails rather than invoking the covered button'sonTap.
Files likely touched
probe_agent/lib/src/executor.dart(_tap,_doubleTap,_longPress, new occlusion helper)probe_agent/lib/src/finder.dart(if the occlusion check lives here instead, alongside_isVisible)probe_agent/test/(new coverage)IMPROVEMENT_TASKS.md(candidate PT-28, following existing convention)
Related but distinct
PT-03 (scroll-to-element / off-screen targeting) covers the target not being in the viewport at all. This issue covers the target being in the viewport but covered by something else at that exact point. Same underlying principle (tap should only succeed where a real user's tap would), different code paths, likely different PRs.
Activity
fix(agent): double tap and long press warn on covered targets (#265);…