Skip to content

refactor(a11y-macos): move the name/description precedence into mapping - #498

Merged
xxx merged 1 commit into
masterfrom
a11y-macos/262-extract-label-precedence
Aug 21, 2026
Merged

refactor(a11y-macos): move the name/description precedence into mapping#498
xxx merged 1 commit into
masterfrom
a11y-macos/262-extract-label-precedence

Conversation

@xxx

@xxx xxx commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #262.

The macOS reader decided a node's name and description inside walkcfg(target_os = "macos"), over a live AXUIElement with no constructible stand-in — so none of the rule could be tested off macOS. The issue's point was that the gate at its heart is behaviourally invisible: hoist the AXDescription read above the titled/untitled decision and every output stays byte-identical while every titled node with help text pays an extra cross-process round-trip. Only a read count catches that.

What moved

mapping::labels(title, read_description, read_help) -> (name, description) — the issue's form 2. The title is a value; the other two are FnOnce readers, so the extraction preserves the reads exactly (same attributes, same order, same counts) and a test can assert which attributes went unread.

mapping::node_name(title, read_description) holds the AXTitle-else-AXDescription precedence that walk, set_value and invoke each spelled out inline. labels delegates its name slot to it rather than restating the rule, so the walk and the two fingerprint sites cannot rank the two attributes differently and reject an element that never moved. The reader is one-shot: whichever slot spends the AXDescription read, the other finds it gone.

normalize_description moves with the decision, which brings the "description repeats the name" drop under test off macOS for the first time.

Whitespace

Pinned as it was, not trimmed. read_label folds "" but not " ", matching nonempty in the Windows and Linux readers. The Android reader deliberately counts a blank label as absent — so this is a live divergence between backends, not a rule this crate settles, and the test says that rather than claiming a convention.

Verification

Ablations, each run and restored:

Ablation Tests that go red
hoist the AXDescription read above the decision a_titled_node_with_help_never_reads_its_description only
retry the help fallback on a label that normalizes away the two ..._without_retrying_the_description tests only
inline normalize_description lookalike: blankness, no trim the combination table only
un-delegate labels and trim its own title arm the_fingerprint_name_and_the_walked_name_agree_on_every_combination

Also: cargo test --workspace green; cargo clippy --workspace --all-targets --locked -- -D warnings clean; cargo clippy --target x86_64-apple-darwin -p glass-a11y-macos --all-targets --locked -- -D warnings clean, which type-checks the cfg(macos) reader from Linux; cargo fmt --all --check clean.

Not run here: crates/glass-macos/tests/a11y.rs, the on-box output check — it needs a TCC-granted Mac. Unchanged by this branch.

No CHANGELOG entry: internal refactor, which the file's own maintenance note excludes.

Known limits

  • labels' two reader parameters are the same type, so transposing them at the call site compiles. One call site, and each closure names its attr:: constant on its own line, so this was judged not worth a newtype.
  • "half the AxTarget fingerprint" is imprecise — name is one of three facts the gate checks — but the phrasing is inherited from glass-core/src/accessibility.rs and reader.rs, so it is left consistent rather than fixed in one place.

Follow-ups found while reviewing, not fixed here

  • read_label's "" fold (reader.rs:370) and read_subrole's gate (reader.rs:411) are the same "delete it and only a read count changes" invariant this issue was filed about, and are still cfg(macos)-only.
  • A whitespace-only name is reachable on macOS, Windows, Linux and iOS, and it displaces a real label: no name: selector reaches it, and outline::is_scaffolding stops eliding the node.
  • walk reads attr::VALUE as a string (reader.rs:477) and gather_states reads the same attribute as an i64 (reader.rs:621), so every checkbox/radio/switch pays two round-trips on one attribute.

…ng (fixes #262)

The macOS reader decided a node's `name` and `description` inside `walk`, which is
`cfg(target_os = "macos")` and takes a live `AXUIElement`, so none of it could be tested off
macOS. The gate at the heart of it is behaviourally invisible: hoist the `AXDescription` read
above the titled/untitled decision and every output stays byte-identical while every titled
node with help text pays an extra cross-process round-trip. Only a read count can catch that
edit.

`mapping::labels` takes the title as a value and the description and help as `FnOnce` readers,
so the extraction preserves the reads exactly — same attributes, same order, same counts — and
a test can assert which attributes went unread. It delegates its name slot to
`mapping::node_name`, which holds the `AXTitle`-else-`AXDescription` precedence that `walk`,
`set_value` and `invoke` each spelled out inline; the reader reaches it through a new
`read_name` helper. The precedence now lives in one place, so the walk and the two fingerprint
sites cannot rank the two attributes differently and reject an element that never moved. Which
attributes each site feeds it is still spelled at that site.

Whitespace-only labels are pinned as they were rather than trimmed: `read_label` folds `""` but
not `"   "`, matching the Windows and Linux readers' `nonempty`. The Android reader counts a
blank label as absent, so this is a live divergence between backends rather than a rule this
crate settles; the test says so.

No behaviour change. Verified by ablation: hoisting the description read fails only
`a_titled_node_with_help_never_reads_its_description`; retrying the help fallback on a label
that normalizes away fails only the two no-retry tests; an inline `normalize_description`
lookalike that tests blankness without trimming fails only the combination table; and
un-delegating `labels` while trimming its own title arm fails the agreement test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xxx
xxx merged commit d65881e into master Aug 21, 2026
18 checks passed
@xxx
xxx deleted the a11y-macos/262-extract-label-precedence branch August 21, 2026 17:11
xxx added a commit that referenced this pull request Aug 21, 2026
… did not carry (#502)

#498's commit message and PR body describe this content; the branch was pushed after an
amend that rewrote the message without staging the working tree, so the panel's fixes never
left the machine. Behaviour is unchanged either way, and #498 as merged is correct — it is the
un-reviewed version of the same refactor. This lands the review.

`labels` delegates its name slot to `node_name` through a one-shot `Option<F>` rather than
restating the `AXTitle`-else-`AXDescription` precedence. The rule now lives in one place
structurally, not by a test that has to enumerate inputs, and the doc no longer claims an
`FnOnce` cannot serve both slots — it can, taken from an `Option`. Read counts and read order
are unchanged: title, then help, then description on a titled node; title, description, help on
an untitled one.

`name` is renamed `node_name`, which `labels` shadowed with its own local binding.

Tests: `an_untitled_node_reads_its_description_exactly_once` is deleted — the `FnOnce` bound
makes a second call a compile error, so the count could not fail and the behaviour is already
row 6 of the combination table. The two dropped-secondary-label tests now pass a *present*
description, so they pin that the fallback does not retry a help label that normalizes away
rather than an absent-value coincidence. The table gains a padded-help row pinning
`normalize_description`'s trim at this seam, and the agreement loops gain blank labels, since a
trim added to one side and not the other is the realistic way the two functions drift.

Docs: `click_element` corrected to `invoke` (the seam method this crate implements); the
extra-read narrative now names the one edit its read count catches, instead of welding that
edit's premise to a different edit's consequence; the unmeasured "most of a macOS tree" is
dropped; and the whitespace rule is described as a live divergence from the Android reader,
which trims deliberately, rather than as a settled cross-backend convention.

Verified by ablation: hoisting the description read fails only
`a_titled_node_with_help_never_reads_its_description`; retrying the help fallback fails only the
two no-retry tests; a blankness-only `normalize_description` lookalike fails only the
combination table; un-delegating `labels` while trimming its own title arm fails the agreement
test.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

a11y-macos: extract the name/description precedence into mapping so it is testable off macOS

1 participant