fix(ios): recover sparse snapshot trees via element queries (React Native)#761
fix(ios): recover sparse snapshot trees via element queries (React Native)#761alexzaetoro wants to merge 3 commits into
Conversation
On some apps (notably React Native), the public XCUIElement.snapshot() traversal collapses to only structural containers (application/window/ other) even though the screen is rendering content with resolvable controls. The runner returned a 2-3 node tree, which broke snapshot, selector resolution, and react-native dismiss-overlay. Detect the sparse-tree signature in snapshotFast and recover through the existing query-based flat traversal (XCUIElementQuery), the same path the collapsed-tab fallback already trusts. The recovered payload is marked truncated and carries an explanatory message; recovery is skipped when queries reveal nothing more than the sparse tree. Adds unit tests for the degenerate-tree detector.
Code reviewFirst off — thank you for the excellent diagnosis. The side-by-side WDA comparison and the patches ruling out idle/watchdog causes are genuinely valuable, and reusing the trusted query path instead of private AX traversal is a reasonable instinct. That said, reviewing the implementation against the actual Findings
Relationship to #758This directly overlaps and textually conflicts with #758, which modifies the same Suggested next steps
Generated by Claude Code |
Problem
On some apps — most reliably React Native / Expo apps —
agent-device snapshoton iOS returns a degenerate 2–3 node tree (application → window [→ other]) even though the screen is foregrounded and fully rendered. The same screen at the same instant exposes 300+ nodes (with alltestIDs as accessibility identifiers) through Appium/WebDriverAgent's/source.Everything that reads the tree is affected: selector
click/find/is/get, andreact-native dismiss-overlay(hangs to the daemon timeout when a LogBox toast is on screen). Coordinate taps are unaffected and work fine (thanks to the quiescence-skip fix in 0.17.0).Root cause (verified)
I patched the shipped runner source locally to rule out the usual suspects:
waitForIdleTimeout = 0(via KVC) around the capture still returns 2 nodes.mainThreadExecutionTimeout30→120s — the snapshot completesok=1inrunner.log, payload still 2 nodes.So the public
XCUIElement.snapshot()API itself returns the sparse tree for these apps. WDA sees the full tree because it bypasses the public snapshot with its own raw accessibility traversal. Notably,XCUIElementQuerystill resolves the controls on the same screen — which this runner already relies on elsewhere (collapsedTabFallbackNodesusesdescendants(matching:.any).allElementsBoundByIndexprecisely because the snapshot tree omits tab children).Change
Rather than introducing a private raw-AX traversal, this reuses the query path the codebase already trusts:
snapshotFastbuilds its node list, detect the sparse-tree signature — only structural containers (Application/Window/Other/ScrollView) with no label/identifier/value and nothing hittable (snapshotNodesAreDegenerate).snapshotFlatInteractivewithinteractiveOnly: false) so on-screen controls andtestIDs come back instead of an empty tree (snapshotQueryRecovery).truncated: trueand carries an explanatorymessage, mirroring the existingsnapshotDepthLimitedAccessibilityFallbackpattern.Public XCUITest APIs only; no new dependencies; the non-sparse path is unchanged (the detector returns early as soon as any real content/control is seen).
Tests
Added unit tests for the detector (
testSnapshotNodesAreDegenerate*) covering: structural-only tree (degenerate), content present, hittable control present, typed control present, and empty input.swiftc -parsepasses on the file.Validation status
/sourcefrom the same screen, runner.log excerpts, and the patch diffs used to rule out idle/watchdog causes) and a perfect repro app — happy to share, and to iterate on the detection heuristic or the recovered payload shape. Opening as draft so you can validate on-device first.Closes the sparse-tree half of the iOS RN snapshot issue.
Made with Cursor