Skip to content

feat(ax): auto-enable Chromium web accessibility tree - #3

Open
caipira113 wants to merge 2 commits into
andelf:mainfrom
caipiralink:feat/chromium-auto-enable-ax
Open

feat(ax): auto-enable Chromium web accessibility tree#3
caipira113 wants to merge 2 commits into
andelf:mainfrom
caipiralink:feat/chromium-auto-enable-ax

Conversation

@caipira113

@caipira113 caipira113 commented Aug 4, 2026

Copy link
Copy Markdown

Problem

Chromium/Electron apps keep their web-content accessibility tree switched off until an assistive client asks for it. Without that opt-in, walking such an app bottoms out at the native window chrome, so snapshot returns only the title bar and menu items and every locator against web content fails.

Before this change, on Microsoft Teams:

- application "Microsoft Teams":
  - window "채팅 | ... | Microsoft Teams"
    - group.RootView:
      - group.NonClientView:
        - group.NativeFrameViewMac:
          - group.ClientView:          <- tree ends here, no web content

This affects Teams, Discord, Slack, VS Code, and Chrome-family browsers, which is a large share of what people actually want to automate.

Change

AXNode::app() is the single place every command resolves an application through, so the opt-in goes there.

  1. Enable. Set AXManualAccessibility (the Electron opt-in) and AXEnhancedUserInterface (the legacy flag some builds expose instead). Both are set unconditionally and their AXError results are ignored: Chromium acts on the attempt while still returning kAXErrorAttributeUnsupported (-25205) or kAXErrorNotImplemented (-25208), so the return code says nothing about whether the tree will appear. Native Cocoa apps reject both and are unaffected.

  2. Settle. The renderer publishes the tree asynchronously over IPC, so a walk starting immediately still sees only the chrome. Poll for an AXWebArea descendant, up to 1.5s.

  3. Gate the wait. Only wait when the app looks Chromium-backed, detected via AXDOMClassList entries on the native chrome (RootView, NonClientView, ClientView). These appear as soon as the window exists, well before web content, which distinguishes "web content is still loading" from "there is no web content" and keeps native apps from paying the timeout.

Verification

Tested on macOS 27.0 (build 26A5388g, arm64). For each run the app was reset to a clean state (AXManualAccessibility = false) so that a single fresh invocation is what gets measured, matching real CLI usage where each command is a new process.

App Engine Before After
Microsoft Teams Edge WebView2 chrome only webarea + full DOM, 0.645s
Discord Electron chrome only webarea + #app-mount, 0.103s
Finder native unchanged unchanged, 0.201s
Microsoft Word native unchanged unchanged, 0.564s

Locators resolve against web content on the first run:

$ axcli --app "Microsoft Teams" snapshot 'textarea'
Resolved → role="AXTextArea" title="" children=1
  - text: "메시지를 입력하세요."  ← matched

cargo test passes (54 tests). cargo clippy reports no new warnings for the changed lines.

Notes

  • If the target app's window lives on another Space that is not currently visible, macOS exposes no window through AX or the CoreGraphics window list at all, and no accessibility flag changes that. activate is needed first. This is orthogonal to this change but easy to mistake for it while testing.
  • Prior art for the enable/fallback/settle sequence: trycua/cua#1756, automattic/harper#3600, and Electron's own accessibility docs.

`AXNode::app` decided whether to wait for web accessibility with
`!has_web_area(..) && looks_chromium(..)`. Both are correct, but they cost
very different amounts on a native app.

`has_web_area` searches twelve levels for an `AXWebArea`. A native app has
none, so the probe cannot short-circuit and walks the entire subtree before
returning false — and Word materialises accessibility elements on demand,
so with a large document open that walk is the whole cost of *every*
command, including ones that never touch the document.

`looks_chromium` reads `AXDOMClassList` at most four levels down and returns
at the first match, and a native app has no such classes at all.

Swapping the operands means a native app is rejected by the cheap test and
never runs the expensive one. The conjunction is unchanged, so Chromium apps
still wait exactly as before.

Measured with a 74k-character document open in Word:

  snapshot --depth 1    15.61s -> 1.10s

No regression elsewhere: Teams still resolves its `webarea` (0.28s) and
Finder is unchanged (0.08s).
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