Skip to content

feat!: flat mobile a11y tree without layout-only nodes - #44

Draft
cryptotavares wants to merge 2 commits into
cryptotavares/mmcli-p1-13-mobile-snapshotfrom
cryptotavares/remove-ax-tree-path
Draft

cryptotavares wants to merge 2 commits into
cryptotavares/mmcli-p1-13-mobile-snapshotfrom
cryptotavares/remove-ax-tree-path

Conversation

@cryptotavares

@cryptotavares cryptotavares commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary: Reduce iOS and Android accessibility output by omitting ancestor path and pruning layout-only elements. Keep named nodes, controls, progress indicators, and unlabelled coordinate targets.

Stack context: mm CLI v2, Phase 1, P1-14. Base: cryptotavares/mmcli-p1-13-mobile-snapshot; review #67 first. Next: #68.

Problem

Mobile path repeats native ancestor class names, such as android.widget.FrameLayout and RCTViewComponentView. These names add no ref or testId targeting information. Unnamed layout wrappers further inflate describe-screen and observation payloads.

Browser ancestry has a different purpose: it holds dialog and heading context used by row collapse. Shared comparison code must accept mobile nodes without losing that browser context.

On iOS, idb also varies frame key order. Equivalent frames can therefore produce different serialized screen signatures.

Solution

Apply pruning at the MobilePlatformDriver normalization boundary, after the app-hierarchy filtering introduced by P1-13.

flowchart TD
    A[App-filtered mobile snapshot] --> B[Prune layout-only elements; hoist kept descendants]
    B --> C[Keep unlabelled RN leaves at least 24 x 24]
    C --> D[Flatten in pre-order; number kept nodes]
    D --> E[Omit path; write bounds in fixed key order]
    E --> F[Build refMap for testId, label, or value]
    F --> G[Mark duplicate stable identifiers as ambiguous]
    A --> H[Read test IDs and locators from app-filtered hierarchy]

    classDef new fill:#fff3cd,stroke:#d39e00
    class B,C,D,E new
Loading

Yellow boxes are new.

pruneLayoutOnly keeps elements with a label, value, identifier, actionable type, or progress-indicator type. It matches exact iOS AX types and Android class-name suffixes. Removed wrappers give way to their kept descendants, in order. The function preserves input objects and returns unchanged subtrees by reference.

A plain React Native view also stays when it has no kept children and measures at least 24×24. This retains unlabelled icon buttons, including the iOS wallet action buttons. These nodes expose bounds for tap-coordinates.

Both getAccessibilityTree and captureScreen use this normalization. Test IDs and generated locators still use the app-filtered hierarchy before layout pruning. Browser nodes retain their path. Shared diffing and row collapse treat two absent paths as equal, but distinguish absence from an empty array.

The filter and screen-settle logic share MOBILE_BUSY_TYPE, so pruning retains the progress indicators that settling detects. Normalization writes bounds in x, y, width, height order to stabilize screen signatures.

What changes

Condition or boundary Before After
iOS and Android accessibility trees Include layout wrappers and every ancestor role in path Omit path; flatten kept nodes in pre-order
Named or identified nodes, controls, and progress indicators Listed among layout wrappers Stay listed; refs number only kept nodes
Unlabelled plain RN views All sizes appear Keep leaves after pruning when both dimensions are at least 24; use their bounds for coordinate taps
Ref targeting Only testId, label, or value creates a refMap entry Same rule; a displayed ref alone does not make an unnamed node targetable
Optional ancestry Shared comparisons require path arrays Accept absent mobile paths; preserve browser ancestry and option-summary paths when present
iOS bounds serialization Uses backend frame key order Always emits x, y, width, height

The original Android-only change (3bbda5c) now covers both mobile platforms under P1-14. The protected proof below records payload reductions and the remaining size-target gaps.

Decisions for reviewers

Decision Reason
Prune in the mobile driver Discovery and observations share one rule. Test ID collection and locator generation keep their existing input.
Use platform-specific control types and a minimum-size RN leaf rule Snapshots have no clickable flag. The leaf rule retains unlabelled wallet action buttons while removing wrappers.
Keep listed nodes separate from targetable refs Progress indicators, unnamed controls, and unlabelled views still provide useful screen or coordinate information. Existing stable-identifier targeting stays intact.
Make the shared path field optional Mobile drops native ancestry. Browser dialog and heading context remains available to row collapse.

Review guide

Read the files in this order:

# File What to check
1 src/tools/types/discovery.ts, src/tools/utils/constants.ts Optional public path, shared progress-indicator matching, and the minimum tap-target size.
2 src/platform/mobile-hierarchy.ts Control classification, descendant order, the RN leaf exception, and unchanged-subtree identity.
3 src/platform/mobile-platform-driver.ts Both normalization entry points, sequential refs, targeting-map eligibility, ambiguity, and ordered bounds.
4 src/server/observation-compaction.ts, src/server/screen-settle.ts, src/tools/utils/discovery.ts Optional-path comparisons, option summaries, shared busy types, and retained browser ancestry.
5 src/platform/mobile-hierarchy.test.ts, src/platform/mobile-platform-driver.test.ts, src/platform/__fixtures__/ios-wallet-home-preorder.json Android login and iOS wallet fixtures, controls, unlabelled action bounds, ref eligibility, and input preservation.
6 Server and discovery tests; README.md, SKILL.md Stable refs across wrapper changes, absent-path diffs, browser paths, and mobile targeting guidance.
Testing

Fresh checks at remote head f38073e4ad5732ade9c22d5f6e486753a44b6416:

CI=true yarn vitest run src/platform/mobile-hierarchy.test.ts src/platform/mobile-platform-driver.test.ts src/server/observation-compaction.test.ts src/server/ref-stability.test.ts src/server/screen-settle.test.ts src/server/create-server.test.ts src/tools/utils/discovery.test.ts --coverage.enabled=false --maxWorkers=2
  • Passes: 7 files, 506 tests. The server suite emits the expected readonly-log EACCES diagnostic.
  • yarn lint passes.
  • yarn tsc --noEmit --project tsconfig.build.json passes for production source.
  • yarn tsc --noEmit --project tsconfig.json fails with 11 existing test-file diagnostics at this unchanged head. They include mock typing, top-level await, and a missing ToolContext import.
  • The Proof and Unit tests sections below retain historical evidence. The fresh checks above cover focused tests, lint, and production types.

Breaking changes

  • A11yNodeTrimmed.path is optional and absent on mobile nodes.
  • Mobile trees no longer include unnamed layout nodes, so mobile ref numbers change. Refs to unnamed nodes never resolved anyway.

Consumer impact

Consumer Change needed Done on branch
Extension test/e2e/playwright/llm-workflow/ None (nothing reads node.path) n/a
Mobile tests/llm-workflow/ None (nothing reads node.path) n/a

Proof

Tested with yalc builds in Mobile (dev client) on the iOS simulator (idb nested format, MetaMask/device-mcp#38) and the Android emulator (MetaMask/device-mcp#39). Before = the #67 build. Byte counts are for the pretty JSON the CLI prints, with compact JSON in brackets.

Check Before (#67) After (f38073e)
iOS wallet home describe-screen 286,526 B, 177 nodes, 6,613 path entries 25,353 B (14,933 compact), 67 nodes, 0
iOS wallet home buttons / tabs / inputs 21 21 (0 missing); the 4 unlabelled action buttons are listed with bounds
Android wallet home describe-screen 987,001 B, 625 nodes, 21,658 path entries 84,271 B (49,772 compact), 0
Android wallet home actionables (incl. every testId) 144 144 (0 missing)
Android click observation: unchanged screen / screen change / full 7,846 / 14,077 / 1,031,678 B 7,849 / 8,047 / 74,963 B (39,690 compact)
iOS unchanged-screen click diff, 5 runs varied by up to 17 nodes empty 5/5 (bounds key order fixed)
Click by ref, 3 targets per platform – iOS 3/3; Android 3/3
Tap a listed action button (Receive) via tap-coordinates at its bounds – opens the receive screen

Acceptance criteria not fully met:

  • iOS describe-screen is ≤ 15 kB only as compact JSON. The CLI pretty-prints with a 2-space indent, which is ~41 % of the bytes.
  • The Android full observation (first click on a screen) is 75 kB, against a 20 kB target. Diffs on later clicks are ~8 kB.

The remaining size is named content (token rows, banners, prices) and bounds (~30 % of mobile bytes). Possible next steps: compact CLI output, dropping or rounding bounds, and a cap on long lists.

Known issues not caused by this PR:

  • device-mcp's findElement accepts a substring testId match and takes the first one, so click --testid account-picker on Android taps account-picker-avatar.
  • The Android unlock click observation still shows "Unlock, busy" under host load.
Unit tests
Test Files  52 passed (52)
     Tests  1803 passed (1803)
Type Errors no errors
All files | 97.51 | 94.88 | 96.73 | 97.66
yarn lint → exit 0

New tests:

  • pruneLayoutOnly, isLayoutOnly, isActionableElement: iOS exact types, XCUIElementType prefix not actionable, Android suffixes incl. CheckedTextView / RatingBar, progress indicators, unlabelled leaf ≥ 24×24.
  • Real iOS wallet-home fixture: 183 → 67 nodes; all buttons and tabs kept; the 4 action buttons listed with bounds.
  • Real Android login fixture.
  • No path on mobile (iOS and Android); browser path kept; diff and collapse with absent path.
  • Bounds key order; shared busy regex.

@cryptotavares
cryptotavares force-pushed the cryptotavares/remove-ax-tree-path branch from 02dbd3a to 3bbda5c Compare August 13, 2026 22:37
@cryptotavares
cryptotavares marked this pull request as ready for review August 14, 2026 06:47
@cryptotavares
cryptotavares requested a review from a team as a code owner August 14, 2026 06:47
Ticket P1-14 (supersedes the Android-only first version of #44).

Mobile a11y nodes carried `path`, the list of every ancestor's role. On
mobile those roles are widget class names (`android.widget.FrameLayout`,
`RCTViewComponentView`), with no targeting value; on Android wallet home
they were 21,658 entries in a ~1 MB describe-screen. Most nodes were also
unnamed layout views.

- `A11yNodeTrimmed.path` is optional. Mobile nodes have no `path` (iOS and
  Android). Browser nodes keep it (ancestor dialog/heading names, used to
  collapse identical rows). The observation diff and row collapse treat two
  absent paths as equal.
- Mobile: layout-only elements (no label, value or testId, not an
  actionable control, not a progress indicator) are dropped and replaced by
  their kept descendants, in order. Containers with a testId, named
  elements, controls and progress indicators stay, and so do unlabelled
  React Native leaf views of at least 24x24 (icon buttons without a label,
  such as the wallet action buttons), listed with bounds.
- Refs exist for nodes with a testId, label or value (as before); listed
  nodes without one (unlabelled tap targets, progress indicators) have
  bounds for tap-coordinates. Ambiguity marking covers only listed nodes.
- `bounds` keys are always in x, y, width, height order (idb returns them
  in varying order, which made unchanged screens look changed).
- One busy-type regex shared by the settle logic and the filter.

BREAKING CHANGE: `A11yNodeTrimmed.path` is optional and absent on mobile
nodes; mobile trees no longer include unnamed layout nodes, so mobile ref
numbers change.
@cryptotavares
cryptotavares force-pushed the cryptotavares/remove-ax-tree-path branch from 3bbda5c to f38073e Compare October 25, 2026 17:41
@cryptotavares cryptotavares changed the title feat: skip a11y ancestor path on android feat!: flat mobile a11y tree without layout-only nodes Oct 25, 2026
@cryptotavares
cryptotavares changed the base branch from main to cryptotavares/mmcli-p1-13-mobile-snapshot October 25, 2026 17:41
@cryptotavares
cryptotavares marked this pull request as draft October 25, 2026 17:41
@cryptotavares
cryptotavares added this pull request to stack #63 October 25, 2026 17:41

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant