Skip to content

fix(android): let a covering sibling hide only what its content covers - #1808

Merged
thymikee merged 4 commits into
mainfrom
claude/issue-1806-fix-verify-713262
Aug 18, 2026
Merged

fix(android): let a covering sibling hide only what its content covers#1808
thymikee merged 4 commits into
mainfrom
claude/issue-1806-fix-verify-713262

Conversation

@thymikee

@thymikee thymikee commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

On Android, snapshot returned a 2–4 node tree for a fully rendered screen whenever a higher drawing-order sibling with sparse content sat over the app UI — a DoraemonKit full-screen drag surface holding one floating icon, or an empty labelled match_parent placeholder container.

pruneAndroidCoveredSubtrees credited a covering sibling with painting its whole box as soon as it had any agent target anywhere inside it, or a label of its own. Both #1806 patterns qualified and took the entire app subtree with them.

Occlusion is now spatial:

  • A subtree's footprint is two sets of rectangles, kept apart rather than merged into a bounding box. Paints — touch targets, scrollables (they consume touches over their box) and labelled leaves — is what a candidate can cover with. Shows adds every labelled or identified node (testID markers, described containers): they paint nothing, so they never help a candidate cover, but an agent would lose them, so they always count toward what a covered sibling has. A full-screen overlay holding one icon paints only that icon; two controls in opposite corners paint two corners, not the screen between them.
  • A sibling is covered when ≥ 90 % of the area of what it shows (or its box, if it shows nothing) lies under what the candidate paints, measured by actual overlapped area (coordinate-compressed cell sweep). Comparing footprint to footprint keeps two stacked screens with matching layout margins registering as covered.
  • A node's own label is no longer paint evidence — a container's content-desc describes its children; an empty labelled View draws nothing. This mirrors [Android] snapshot returns 1 node on Telegram while stock uiautomator dump sees the full tree (descent stops at depth 4) #1733 (focusability is traversal, not paint). Only a touch target still hides its full box, so a tap-to-dismiss scrim keeps condemning what's under it.

No flag, no hint: the defect can't be expressed rather than being detected.

Closes #1806.

Validation

Live, Pixel 9 Pro XL API 37 emulator. Added a DoKit-shaped overlay to the test app (full-screen non-clickable View with accessibilityLabel, one small Pressable inside; not committed) and drove the built CLI:

# pre-fix parser
Snapshot: 2 nodes
Hint: sparse accessibility snapshot returned 2 nodes; snapshot state is invalid or unavailable…
@e1 [group] "DK"
@e2 [text] "DK"

# this branch
Snapshot: 13 visible nodes (37 total)
@e2 [scroll-area] "home-title, Agent Device Tester, 0 in cart, …" [scrollable]
@e11 [button] "Drag source" … @e35 [group] "Settings" @e36 [group] "DK"

Helper-XML A/B (same XML through pre-fix and fixed parser, raw + interactive): with the overlay, home / catalog / form / product-detail all went from the same 45-node skeleton (21 interactive) to 116 / 125 / 111 / 83 nodes with every app label recovered and none lost. Product detail (a native-stack push over the Catalog tab) shows only the pushed screen — no Catalog leak. Settings, Chrome, launcher and the pre-collapsable={false} build (where RN flattened the overlay away) are byte-identical old vs new.

Unit. Six new fixtures fail on the pre-fix parser: the DoKit overlay, the empty labelled placeholder, a sparse-overlay-vs-rich-sibling footprint case, an overlay whose only controls sit in opposite corners (also fails on the first revision's bounding-box model, which is what it pins), a container of testID markers with one covered corner icon (fails on the second revision's single-footprint model), and a focusable full-screen wrapper holding one clickable icon over app content (fails if focusability is allowed to paint a candidate's box). Three existing fixtures were the exact synthetic shape the fix stops calling covered (a full-screen box with one small element over a full-screen box with an element elsewhere) and were rewritten to realistic covering content — a pushed screen with header + scrollable body + footer, a full-width tile, and a clickable scrim; the boundary they pin still holds.

Fuzz. 20 k random 2–3-sibling trees over the attribute space (bounds incl. child overflow, drawing-order, clickable/focusable/scrollable, text, resource-id), pre-fix vs fixed parser, disagreements classified by direction. "New prunes more" — the only direction that can produce a new sparse-snapshot bug — is 0.14 % with children clipped to parents (0.8 % with overflow), and every inspected case is the intended rule: everything the target shows lies fully under a higher touch/scroll surface or an overflowing labelled leaf. The rest is the pruner being more conservative.

check:affected set green locally: format, lint, typecheck, layering, fallow, build, vitest-related.

Notes

  • Not done (out of scope): the reporter's escape-hatch flag and a "pruned by occlusion" hint. Both would be detectors for a condition the parser no longer produces.
  • Live-run quirk hit along the way, unrelated: open immediately after close on the same emulator reported DEVICE_IN_USE by the just-closed session for a couple of seconds.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.28 MB 2.28 MB +1.2 kB
JS gzip 748.3 kB 748.7 kB +401 B
npm tarball 870.1 kB 870.5 kB +391 B
npm unpacked 3.04 MB 3.04 MB +1.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.5 ms 28.7 ms +0.2 ms
CLI --help 67.6 ms 68.1 ms +0.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/snapshot.js +1.1 kB +434 B
dist/src/prepare-kind.js +207 B +30 B
dist/src/internal/daemon.js +6 B +9 B
dist/src/runtime.js +6 B +8 B
dist/src/session2.js 0 B -4 B

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed exact head 2d3ccdf. P1 remains in the new footprint model: subtreeFootprint unions all presented descendants into one enclosing rectangle. A transparent full-screen overlay with two small controls in opposite corners therefore acquires a viewport-sized footprint and can prune the full app below, despite painting only two tiny regions—the same snapshot-collapse class as #1806. Add a regression for disconnected sparse overlay controls and model actual covered rects/union area (or conservatively refuse sparse/disconnected candidates) rather than their bounding box. Exact-head CI is green and the single-icon/empty-placeholder cases are otherwise well covered.

thymikee added a commit that referenced this pull request Aug 18, 2026
Review on #1808: a bounding box of two corner controls spans the
viewport, so a transparent overlay with a control in each corner still
acquired a full-screen footprint and could prune the app beneath it.

Footprints now keep their presented rects apart, and coverage is the
overlapped area of the two unions (coordinate-compressed cell sweep).
Scrollables count as presenting their box: they consume touches over it,
which is what lets a real pushed screen (header, scrollable body, footer)
still cover a drawer surface. Adds the disconnected-corner regression.
@thymikee

Copy link
Copy Markdown
Member Author

Addressed in e1e706d — thanks, the enclosing rectangle was the wrong primitive.

  • subtreeFootprint now keeps the presented rects apart (Rect[]) instead of merging them, and unionCoverage measures the actual overlapped area of the two unions via a coordinate-compressed cell sweep. Two corner controls present two corners, not the screen between them.
  • Scrollables count as presenting their box (they consume touches over it). That is what lets a genuine pushed screen — header, scrollable body, footer — still cover a drawer surface by real area; the "pushed screen" fixture gained that body.
  • New regression: keeps app content under an overlay whose only controls sit in opposite corners — fails on the previous commit's bbox model, passes now.

Re-ran the helper-XML A/B over the Pixel 9 captures against pre-fix: overlay screens still recover everything (45→116/125/111/83), nothing lost anywhere. One new one-node delta on the launcher: the labelled "Google search" search-pill background is no longer condemned by the icons at its ends — a real visible element, so keeping it is correct. Product-detail stack push is unchanged (the pruner is idle there; react-native-screens handles it).

Local gates green: format, lint, typecheck, layering, fallow, android unit suites.

@thymikee

Copy link
Copy Markdown
Member Author

Clean re-review at e1e706d: the prior bounding-box blocker is resolved with disconnected footprint rectangles and actual union-overlap coverage; the opposite-corner regression fails the prior model and the updated body now describes the implementation and four fixtures accurately. Live Pixel/helper-XML evidence remains relevant, exact-head CI including Android Smoke is green, and merge state is CLEAN. Ready-for-human.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 18, 2026
@thymikee

Copy link
Copy Markdown
Member Author

One more revision (5be8ba0), prompted by fuzzing the pruner rather than by a report.

I generated 20 k random 2–3-sibling trees over the attribute space and diffed pre-fix vs fixed, classifying disagreements by direction. The only direction that can create a new sparse-snapshot bug is "new prunes something old kept". That surfaced one real gap: a container whose only painted content is small (a corner icon) but which also carries testID-only markers or labelled containers was condemned as soon as a touch surface covered the icon — markers and container labels aren't paint, so they never entered the footprint.

Fix: footprints now carry two sets. paints (touch targets, scrollables, labelled leaves) is what a candidate can cover with — still excludes identifiers/container labels, or the DoKit fix would unwind. shows adds every labelled or identified node and is what a covered sibling must lose in full. Also aligned focusable-only descendants with #1733: they no longer paint their box. Complexity split into footprintWithinBox/childrenFootprint for fallow.

After this, new-prunes-more is 0.14 % of random trees with children clipped to parents, and every inspected case is the intended rule (everything the target shows lies under a higher touch/scroll surface). Live Pixel 9 captures unchanged from the previous revision. New unit fixture pins the marker case (fails on the previous commit). PR body updated to describe paints/shows and the fuzz.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 18, 2026
@thymikee

Copy link
Copy Markdown
Member Author

New delta at 5be8ba0 is not ready. paintsOwnBox changes focus-only nodes from full-box paint to non-painting, but no regression proves that behavior: the new identifier-marker test exercises shows, and the existing childless-focusable case never forms a covering candidate. Add a full-screen focusable container with a small clickable descendant over a lower sibling and prove the lower content survives (and that reverting this focusability change fails). Exact-head Coverage is also red on the inherited hover frameworkTier parity failure, so rebase after #1817 and rerun.

pruneAndroidCoveredSubtrees credited a higher drawing-order sibling with
painting its whole box as soon as it had any content anywhere inside it,
or a label of its own. A full-screen DoraemonKit drag surface holding one
189px floating icon therefore condemned the entire app subtree, and an
empty labelled match_parent placeholder did the same.

Occlusion is now spatial. A subtree's footprint is the bounding box of
what it presents (agent targets and labelled leaves); a sibling is covered
when its footprint lies under a candidate's footprint. A node's own label
is no longer paint evidence: a container's content-desc describes its
children and an empty labelled View draws nothing. Only a touch target
still hides its full box (scrims). Comparing footprint to footprint keeps
stacked screens with matching margins registering as covered.

Live on a Pixel 9 Pro XL API 37 emulator with a DoKit-shaped overlay
added to the test app: snapshot -i went from 2 nodes + the sparse hint to
the full app; helper-XML A/B across home/catalog/form/product-detail
recovered every label with none lost, and non-overlay screens are
byte-identical.
Review on #1808: a bounding box of two corner controls spans the
viewport, so a transparent overlay with a control in each corner still
acquired a full-screen footprint and could prune the app beneath it.

Footprints now keep their presented rects apart, and coverage is the
overlapped area of the two unions (coordinate-compressed cell sweep).
Scrollables count as presenting their box: they consume touches over it,
which is what lets a real pushed screen (header, scrollable body, footer)
still cover a drawer surface. Adds the disconnected-corner regression.
…ints

Fuzzing random sibling trees old-vs-new surfaced the one direction the
footprint model could still regress: a container whose only painted
content is small (one corner icon) but which also carries labelled
containers or testID-only markers was condemned as soon as a touch
surface covered that icon, since markers and container labels are not
paint and never entered the footprint.

Footprints now carry two rect sets. `paints` (touch targets,
scrollables, labelled leaves) is what a candidate can cover with; it
still excludes identifiers and container labels, or the DoKit fix would
unwind. `shows` adds every labelled or identified node and is what a
covered sibling must lose in full. Focusable-only nodes no longer paint
their box either, matching #1733 for descendants as well as siblings.

Adds the marker regression. Re-fuzzed 20k trees: new-prunes-more is down
to 0.14 %, all of the class where everything the target shows lies under
a higher touch/scroll surface. Live captures unchanged.
…e's box

A full-screen focusable wrapper holding one clickable icon is a covering
candidate; the lower app content must survive. Fails when paintsOwnBox
counts focus targets again.
@thymikee
thymikee force-pushed the claude/issue-1806-fix-verify-713262 branch from 5be8ba0 to 189990a Compare August 18, 2026 12:15
@thymikee

Copy link
Copy Markdown
Member Author

Both points addressed at 189990a.

PR body updated to list the sixth fixture.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 189990a: the added focusable-wrapper regression closes the prior gap and is non-vacuous—restoring the old full-box paint behavior prunes the lower content. The delta is test-only, the PR body is current, and Coverage plus all completed gates are green. Code review is clean and ready-for-human; iOS Smoke is still in progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Contentless full-screen containers count as occluders, pruning the entire app tree from snapshot

1 participant