Skip to content

Consumer Coverage and a11y Flag Wiring#731

Draft
William-Selna wants to merge 7 commits into
AccessKit:mainfrom
William-Selna:consumer-coverage-and-a11y-flag-wiring
Draft

Consumer Coverage and a11y Flag Wiring#731
William-Selna wants to merge 7 commits into
AccessKit:mainfrom
William-Selna:consumer-coverage-and-a11y-flag-wiring

Conversation

@William-Selna

@William-Selna William-Selna commented Jun 24, 2026

Copy link
Copy Markdown

In this PR

7 commits - I originally planned on purely bumping function test coverage, but I found an odd twist in the wiring and a few stubs from 2022. At first, I intended these to be largely conformance tests (as in look at the asserted behavior), but I was skeptical of the insertion and decided to investigate.

The test I'm referring to is page_navigation_resolves_to_document_bounds(), a test to assert the following 3 position methods:

  • forward_to_page_start
  • forward_to_page_end
  • backward_to_page_start

As was originally written, the test I wrote asserted page-start == page-end == document_end, which appears to me as counterintuitive. So I decided to look further. Pulling up the blame, it appeared as an unwired artifact from 2022. After noticing these stubs, I ran a check I'd written for some of my other projects that flags write-only state, ones settable by a provider, but not read by anything.

The test/grep flagged is_page_breaking_object as write-only, which is the exact flag these methods would need to consult. That explains the four stubs below. With nothing reading the flag, they could only ever resolve to document bounds.

pub fn forward_to_page_start(&self) -> Self { self.document_end() }
pub fn forward_to_page_end(&self)   -> Self { self.document_end() }
pub fn backward_to_page_start(&self) -> Self { self.document_start() }
pub fn is_page_start(&self) -> bool  { self.is_document_start() }
// no is_page_end, no backward_to_page_end

Open Decision Pending: is_page_breaking_object

The schema documents is_page_breaking_object only as "Indicates whether this node causes a page break," and no provider sets it today, so nothing pins down the tree shape a provider is expected to emit. Implementing detection requires
committing to a contract, and I assumed: A node carrying is_page_breaking_object is a container wrapping a page's
content, and the break occurs before it
— so the node's first text-run descendant begins a new page (starts_new_page walks up from a run to find such an ancestor.)

Alternatives I did not assume: break-after (the flagged node ends a page), or the flag on a marker/separator node between pages — cf. is_line_breaking_object, documented as "block level elements, or <br>," which spans both readings.

If you intended a different contract, only the detection predicate changes, and the navigation loops and tests are unaffected. The implementation is on the branch if you want it; the contract choice (break-before / break-after / marker-node) is yours, and if you'd rather not commit to page semantics this cycle, the cleanest answer is to drop these commits and keep the rest. I'm also happy to split the PR up, I know the surface is on the larger side.

The Fixes for the Stubs:

  • is_page_start → now true at the document start or at a line-start that begins a new page (detected by walking up to an ancestor flagged is_page_breaking_object).
  • is_page_end → new method, mirroring is_paragraph_end.
    forward_to_page_start / forward_to_page_end / backward_to_page_start → the one-line stubs became real loops that walk line-by-line to the next page boundary, structurally identical to the existing paragraph-nav methods.
  • Added the detection helpers (starts_new_page, next_run_starts_new_page, node_contains).
  • Deliberately did not add backward_to_page_end — no text unit in the crate has a backward-to-end method, so adding one would make page the inconsistent unit.
  • Backward-compatible: with no is_page_breaking_object flags anywhere (every document today), all of these collapse back to exactly the old document-bounds behavior — so nothing regresses; only trees that set the flag get the new behavior.
  • The methods now consume the is_page_breaking_object schema flag, which previously existed but was read by nothing.

Wiring other write-only flags.

The same unwired-detector run surfaced more schema flags that were set-able but read by nothing. Three had a real platform home, so this PR wires them:

  • is_visited → AT-SPI State::Visited (already-followed links now announce as visited).
  • is_spelling_error / is_grammar_error → text attributes: AT-SPI invalid = "spelling"/"grammar", and the Windows UIA AnnotationTypes attribute.
    macOS arms are deferred — its text-attribute path is about to be reworked by the pending objc2 upgrade (#616).

Tests Written to Assert new Behavior

consumer/src/text.rs (13)

Page navigation:

  • is_page_start_tracks_breaks_not_paragraphs
  • forward_to_page_start_skips_intra_page_paragraphs
  • forward_to_page_end_lands_on_page_boundaries
  • backward_to_page_start_walks_pages_in_reverse
  • without_page_breaks_navigation_resolves_to_document_bounds

Spelling / grammar (auto-generated by inherited_flags!):

  • is_spelling_error::directly_set
  • is_spelling_error::mixed
  • is_spelling_error::set_on_parent
  • is_spelling_error::unset
  • is_grammar_error::directly_set
  • is_grammar_error::mixed
  • is_grammar_error::set_on_parent
  • is_grammar_error::unset

platforms/atspi-common/src/text_attributes.rs (1)

  • invalid_attribute_reflects_error_flags

platforms/atspi-common/src/node.rs (1)

  • visited_flag_maps_to_atspi_visited_state

Overall accesskit_consumer Coverage improvement

File Line Function
node.rs 79.2% → 96.7% 57.4% → 96.0%
text.rs 90.6% → 96.4% 82.1% → 97.7%
tree.rs 91.2% → 93.0% 70.2% → 74.5%
Total 89.6% → 95.6% 72.9% → 90.1%

Notes

The Windows AnnotationTypes arm is type-checked against x86_64-pc-windows-msvc but I have no Windows host, so its runtime behavior relies on CI.

I can also split this PR up into just tests or however you like, please don't hesitate to let me know.

Adds direct tests for the numeric/scroll/state accessors plus a broad
accessor sweep (supported actions, labels, descriptions, set membership,
role-derived predicates, values, read-only/disabled composition, and
hidden-flag inheritance). Lifts node.rs from 57% to ~97% function coverage.
Adds the Range/WeakRange weak-reference round trip (including the degenerate
range and refuse-to-upgrade-against-a-non-text-node cases), the Position
page-navigation/document-bound helpers, and the node-level text-selection
accessors. Lifts text.rs to ~97% line / ~99% function coverage.
Exercises is_host_focused, focus_in_tree, active_dialog, toolkit_name,
toolkit_version, and the update_host_focus_state_and_process_changes path.
Replaces the placeholder page_navigation_resolves_to_document_bounds
test, which pinned the degenerate stub behavior, with a real
implementation and behavioral tests.

Position's page-navigation methods were stubs that resolved every page
motion to a document boundary, ignoring the is_page_breaking_object node
flag the schema already defines. This wires them to honor real page
breaks.

Page boundaries are detected from block structure (a Position's text run
has an ancestor flagged is_page_breaking_object whose subtree excludes
the preceding run), unlike line boundaries (run links) or paragraph
boundaries (a run value ending in '\n'). The break is treated as
occurring before the flagged node.

- is_page_start and the new is_page_end consult the page-break flag.
- forward_to_page_start, forward_to_page_end, and backward_to_page_start
  walk page boundaries, mirroring the paragraph-navigation methods.
- A document with no page-break flags is a single page, so behavior is
  unchanged for every existing provider; only trees that set the flag
  exercise the new path.

The Windows UIA TextUnit_Page mapping inherits this for free; macOS and
AT-SPI have no page text granularity and are unaffected.
The CI fmt job (cargo fmt --all -- --check) rejected three spots in the
consumer test code: unsorted imports in node.rs, plus an assert_eq! and
a use list that rustfmt wraps across lines. Normalize them so the check
passes. No behavior change.
A node flagged is_visited (e.g. a hyperlink the user has already
followed) now maps to the AT-SPI Visited state. The flag was previously
write-only: providers could set it but no adapter read it, so on Unix a
visited link was indistinguishable from an unvisited one.

- consumer: add Node::is_visited(), matching the sibling flag getters.
- atspi-common: insert State::Visited in NodeWrapper::state() alongside
  the other flag-driven states, with a test covering both directions.

State::Visited is the natural target (atspi-common documents it as a
hyperlink that has already been activated). Windows has no per-node
visited concept; macOS gains one only once objc2-app-kit reaches 0.3
(NSAccessibilityVisitedAttribute), which is gated on the pending objc2
upgrade, so both are left untouched here.
Nodes flagged is_spelling_error / is_grammar_error now surface through
the text-attribute pipeline so assistive technology can announce
misspelled and grammatically incorrect spans. Both flags were
previously write-only: providers could set them but no adapter read
them.

- consumer: treat the flags as inherited text attributes (exposing them
  on Range/Position) and split text runs on them.
- AT-SPI: report the "invalid" attribute ("spelling" / "grammar").
- Windows: report the UIA AnnotationTypes attribute (AnnotationType_
  SpellingError / GrammarError) as an array, using the reserved "mixed"
  value when a flag varies across the range.

The consumer and AT-SPI paths are covered by tests; the Windows arm is
type-checked against the windows-msvc target but relies on CI for
runtime verification, since this was authored without a Windows host.
The macOS adapter is left untouched: its text-attribute path is about to
be reworked by the pending objc2 upgrade (AccessKit#616), so
the marked-misspelled arm is better added on top of that.
@William-Selna William-Selna marked this pull request as ready for review June 24, 2026 14:56
@DataTriny

Copy link
Copy Markdown
Member

Hello @William-Selna, thank you for your interest in helping here.

Unfortunately this pull request contain too much and barely related changes. Especially important for a first time contributor, please focus on contained changes that will be easier to review.

Most of your tests also do too much, prefer smaller unit tests that would be easier to understand if they break. Testing very simple methods for the sake of increasing coverage is not a priority either.

When exposing node properties through the platform adapters, remember that Chromium is our reference implementation. Try to handle as much platforms as possible, and link a test program using the accesskit_winit crate to your pull request, so we can at least perform manual testing.

I am going to keep this pull request open for now, letting you decide how you want to re-scope it. But I won't review it as is. Thank you for your understanding.

@William-Selna William-Selna marked this pull request as draft June 26, 2026 22:47
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.

2 participants