Files
Michał Pierzchała 294654a3a5 fix(android): resolve snapshot scope once and disclose the API 23 occlusion-scan gap (#1832 C1/C2) (#1846)
* fix(android): resolve snapshot scope once and disclose the API 23 occlusion-scan gap (#1832 C1/C2)

- Android resolves --scope inside its projection only, under the shared scope specification
  (matchesSnapshotScope in @agent-device/contracts/snapshot: first document-order match over
  label/value/identifier, empty on no match). The daemon post-wire scopeSnapshotNodes pass skips
  the android backend, so scope has one owner and one no-match semantics instead of BFS+fallback
  followed by document-order+empty.
- Golden table contracts/fixtures/snapshot-scope-policy.json is asserted against the predicate,
  the Android projection, and the daemon pass; the Swift runner twin (#1797) consumes the same table.
- androidSnapshot.occlusionScanUnavailable discloses helper trees without drawing-order (API 23),
  where the covered-sibling pruner cannot run. Disclosure only; C1 stays open until occlusion moves
  to the daemon annotator.

* fix(android): resolve scope over the presented tree and stop dropping it on interaction captures

Adversarial review findings on the first commit:

- BLOCKER: captureSnapshotData spread `snapshotScope: undefined` over flags, so an interaction
  capture (press/click/fill/longpress/hover --scope, --settle observation) reached the Android
  platform unscoped while buildSnapshotState still saw the scope. The post-wire pass used to rescue
  it; after skipping android it returned the unscoped tree. One effective scope now feeds both.
- Scope resolves over the PRESENTED nodes of the requested projection, not the acquired tree, so an
  acquired match that membership drops no longer empties the snapshot, and Android matches the
  domain iOS's pass uses.
- Slicing after the walk keeps ancestor context (hittable/collection/chrome) above the scope root,
  so scoped -i is a subset of unscoped -i; --depth stays scope-relative.
- Shared findSnapshotScopeRange/reindexSnapshotNodes so the daemon pass and the Android projection
  run one implementation; scope slice extracted to ui-hierarchy-scope.ts (mirrors its test).
- parseUiHierarchy moved to a test fixture module (it had no production caller left).
- Golden rows sharpened (value row no longer matches via label on Android); the Android leg runs
  raw AND regular. CHANGELOG entry; docs wording corrected for iOS/@ref.

* fix(layering): keep the contracts snapshot façade exhaustive over snapshot-scope

* fix(android): scope to the first match whose subtree still has presented content

Review P1 on #1846: with scope resolved strictly over presented nodes, `snapshot -i --scope panel`
answered "no nodes" whenever the matched container was a structural view membership drops — even
though the button inside it was exactly what was asked for — and `--depth 0` then hid a node the
response prints at depth 0.

The scope root is now the first document-order match whose subtree contributes at least one node to
the requested projection, and the result is that subtree's presented nodes re-rooted at depth 0.
Both failure modes die: a decorative match membership drops no longer empties the snapshot, and a
dropped container still scopes to its content. `--depth` under scope filters the depths the
response emits, so a node shown at depth 0 survives `--depth 0`.

Tests: the golden legs stay raw+regular (bare TextViews cannot survive -i, so an -i leg would
measure membership, not scope) with the projection interplay pinned by two dedicated tests on
actionable shapes; the 'not re-scoped after the wire' case now runs a real parsed scoped tree
instead of fabricated depth-0 siblings.
2026-08-18 18:44:13 +02:00
..