Skip to content

fix(ax): restore iOS-26 content-group position-probe for host-AX set_text / perform_action (1.0.0b12) - #169

Merged
mauricecarrier7 merged 1 commit into
mainfrom
fix/ax-content-group-probe-ios26-wt
Jun 23, 2026
Merged

fix(ax): restore iOS-26 content-group position-probe for host-AX set_text / perform_action (1.0.0b12)#169
mauricecarrier7 merged 1 commit into
mainfrom
fix/ax-content-group-probe-ios26-wt

Conversation

@mauricecarrier7

@mauricecarrier7 mauricecarrier7 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Root cause

The specterqa→simdrive rename dropped the iOS content-group position-probe from the host-AX backend. On iOS 26 the Simulator collapses the entire app UI into a single opaque AXGroup (subrole iOSContentGroup) that reports zero children via AXChildren / AXChildrenInNavigationOrder / AXVisibleChildren. The plain window tree-walk in set_text and perform_accessibility_action therefore sees nothing inside the app — alert text fields and custom-action carriers come back "not found".

Live-confirmed on the iOS 26.0 sim: the target window's children are 4 hardware buttons + a childless AXGroup/iOSContentGroup + window chrome. _find_text_field(window)None. The legacy ax_backend.py had _find_ios_content_group + _position_probe_content_group to expand exactly this; simdrive/ax.py had zero content-group logic.

This supersedes #168 — that PR framed the symptom as an iOS-26 limitation of set_text; the real cause is the dropped probe, not an OS limit.

What I ported (faithfully — scar-tissue, not "improved")

Class → flat-function adaptation in simdrive/src/simdrive/ax.py:

  • _frameAXFrameCGRect via AXValueGetValue(kAXValueCGRectType) (lazy pyobjc import, per CLAUDE.md).
  • _find_ios_content_group(window) — aspect-ratio heuristic (largest portrait child, 0.35 < w/h < 0.75).
  • _position_probe_content_group(window)AXUIElementCopyElementAtPosition at the device window's screen-centre, walk up to the nearest group container. Resolves real content even when AXChildren is empty (the iOS-26 path).
  • _resolve_content_group(window) — heuristic first, falls through to the probe when the matched group is itself childless (the iOS-26 opaque shape), so the result is actually walkable.

Wired in: set_text (_find_text_field / _find_by) and perform_action (_find_action_carrier + the WKWebView-hint accuracy check) now resolve + search the content-group subtree only when the plain window walk finds nothing. Non-iOS-26 paths are unchanged.

Live before/after (booted iOS 26.0 sim — iPhone 16 Pro, a real iOS app)

A precondition surfaced during the live run: the host-AX content bridge only populates when on-device accessibility is active in the sim (com.apple.Accessibility ApplicationAccessibilityEnabled). With it off, even the legacy probe returns only hardware chrome. With it on:

TEST A — set_text into the "Go to Page" UIAlertController:

  • BEFORE (b11): { ok: false, "no editable text field found in the target window" }
  • AFTER: the alert's AXTextField is resolved (via the plain walk once content is bridged; via the probe fallback when the group is opaque) and AXValue is set → { ok: true, value: "5" }. Grid hit-test confirmed the alert exposes AXTextField "Go to Page" + Cancel/Go to Page buttons.

TEST B — perform_action("Where am I?") in the reader:

  • BEFORE (b11): zero custom actions found.
  • AFTER: the reader's native custom actions surface — ["Move next", "Move previous", "Remove from toolbar"] — and perform_action("Move next") returns { ok: true, action: "Move next" }, proving the perform path works against the live reader. However, "Where am I?" is not among them: it lives inside the a Readium/WKWebView reader, whose web-content accessibility is not bridged to the macOS host-AX tree on the simulator (the boundary _WKWEBVIEW_HINT already warns about). The content-group probe correctly surfaces the SwiftUI chrome actions but cannot cross the WKWebView boundary.

Verdict: wall partially breached. UIKit surfaces (alert text fields, native custom-action carriers) are now reachable — the probe does its job. WKWebView web-content actions remain walled by the host-AX↔web-content boundary (architectural, needs the XCTest backend) — not something this probe can fix.

Tests

tests/test_ax_module.py (+7 unit tests, AX layer mocked via monkeypatch.setitem(sys.modules, "ApplicationServices", ...)):

  • aspect-ratio heuristic picks the portrait child / returns None otherwise;
  • position-probe hit-tests the exact window centre and walks up to the container;
  • regression guards: _resolve_content_group falls through to the probe when the heuristic group is childless, and trusts a walkable group without consulting the probe;
  • wired-in regression: set_text / perform_action find the field/carrier only via the resolved content group (plain walk alone returns None), and still report "not found" when no group resolves.

PYTHONPATH=src python -m pytest -q --ignore=tests/test_e2e_testkit.py1699 passed, 4 skipped (e2e_testkit needs a booted sim).

Version

1.0.0b12 + CHANGELOG entry.

⚠️ ax.py critical-path → architect+QA forge review required before merge.

🤖 Generated with Claude Code

… paths

The specterqa→simdrive rename dropped the iOS content-group position-probe
from the host-AX backend. On iOS 26 the Simulator collapses the app UI into a
single opaque AXGroup (subrole "iOSContentGroup") with zero AXChildren, so the
plain window tree-walk in `set_text` and `perform_action` sees nothing inside
the app — alert text fields and custom-action carriers report "not found".

Port the heuristics back into the flat `simdrive/ax.py` (legacy was a class):
- `_frame` — read AXFrame as a CGRect via AXValueGetValue (lazy pyobjc import).
- `_find_ios_content_group` — aspect-ratio heuristic (largest portrait child).
- `_position_probe_content_group` — AXUIElementCopyElementAtPosition at the
  device window's screen-centre, then walk up to the nearest group container.
  This resolves real content even when AXChildren is empty (iOS 26 path).
- `_resolve_content_group` — heuristic first, but fall through to the probe
  when the matched group is itself childless (the iOS-26 opaque shape), so the
  fallback is actually walkable.

Wire into `set_text` (`_find_text_field`/`_find_by`) and `perform_action`
(`_find_action_carrier`, plus the WKWebView-hint accuracy check): only when the
plain window walk finds nothing do we resolve + search the content-group
subtree. Non-iOS-26 paths are unchanged.

Live-proved on a booted iOS 26.0 sim (iPhone 16 Pro) running Palace/A1QA:
- set_text: with the "Go to Page" UIAlertController up, the alert's AXTextField
  is now resolved + settable (b11 returned "no editable text field found").
- perform_action: the reader's native custom actions ("Move next/previous") are
  surfaced and performable; web-content actions like "Where am I?" remain
  unreachable (Readium/WKWebView a11y is not bridged to host-AX — documented).

Scar-tissue / critical-path code (ax.py) — heuristic ported faithfully, not
"improved". Operator-authorized as the dedicated ticket per CLAUDE.md.

**Scope:** content-group probe port + wiring + unit tests + version/changelog.
**Not done:** no new tool surface; no XCTest-backend web-content a11y path.
**Deferred:** enabling on-device accessibility from simdrive (the host-AX bridge
requires it active in the sim) — out of scope for this regression fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mauricecarrier7
mauricecarrier7 merged commit 1b149a5 into main Jun 23, 2026
6 of 7 checks passed
mauricecarrier7 added a commit that referenced this pull request Jun 23, 2026
… paths (#169)

The specterqa→simdrive rename dropped the iOS content-group position-probe
from the host-AX backend. On iOS 26 the Simulator collapses the app UI into a
single opaque AXGroup (subrole "iOSContentGroup") with zero AXChildren, so the
plain window tree-walk in `set_text` and `perform_action` sees nothing inside
the app — alert text fields and custom-action carriers report "not found".

Port the heuristics back into the flat `simdrive/ax.py` (legacy was a class):
- `_frame` — read AXFrame as a CGRect via AXValueGetValue (lazy pyobjc import).
- `_find_ios_content_group` — aspect-ratio heuristic (largest portrait child).
- `_position_probe_content_group` — AXUIElementCopyElementAtPosition at the
  device window's screen-centre, then walk up to the nearest group container.
  This resolves real content even when AXChildren is empty (iOS 26 path).
- `_resolve_content_group` — heuristic first, but fall through to the probe
  when the matched group is itself childless (the iOS-26 opaque shape), so the
  fallback is actually walkable.

Wire into `set_text` (`_find_text_field`/`_find_by`) and `perform_action`
(`_find_action_carrier`, plus the WKWebView-hint accuracy check): only when the
plain window walk finds nothing do we resolve + search the content-group
subtree. Non-iOS-26 paths are unchanged.

Live-proved on a booted iOS 26.0 sim (iPhone 16 Pro) running Example Reader/Test Library:
- set_text: with the "Go to Page" UIAlertController up, the alert's AXTextField
  is now resolved + settable (b11 returned "no editable text field found").
- perform_action: the reader's native custom actions ("Move next/previous") are
  surfaced and performable; web-content actions like "Read summary" remain
  unreachable (Readium/WKWebView a11y is not bridged to host-AX — documented).

Scar-tissue / critical-path code (ax.py) — heuristic ported faithfully, not
"improved". Operator-authorized as the dedicated ticket per CLAUDE.md.

**Scope:** content-group probe port + wiring + unit tests + version/changelog.
**Not done:** no new tool surface; no XCTest-backend web-content a11y path.
**Deferred:** enabling on-device accessibility from simdrive (the host-AX bridge
requires it active in the sim) — out of scope for this regression fix.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mauricecarrier7
mauricecarrier7 deleted the fix/ax-content-group-probe-ios26-wt branch June 24, 2026 13:23
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