Skip to content

Resilient a11y-tree navigation for Firefox address field and Thunderbird recipients#21

Open
akj wants to merge 2 commits into
javidominguez:masterfrom
akj:feature/resilient-address-field
Open

Resilient a11y-tree navigation for Firefox address field and Thunderbird recipients#21
akj wants to merge 2 commits into
javidominguez:masterfrom
akj:feature/resilient-address-field

Conversation

@akj

@akj akj commented Jun 4, 2026

Copy link
Copy Markdown

Why

Mozilla restructures Firefox's and Thunderbird's IAccessible2 trees between releases, and the add-on navigated them with searchObject(path) — a rigid sequence of (attr, value) milestones matched against direct children only, returning on the first miss. The moment Mozilla inserts a wrapper element, the path silently breaks and the feature dies with a generic "not found".

FF151 did exactly this: it wrapped #urlbar-input in a new .urlbar-input-container, so NVDA+A ("read address bar") started saying "Address not found". The same change also silently broke the notification domain-tagging path in event_alert.

What

A resilient subtree-search primitive (shared): findInSubtree(anchor, predicate) — an anchored descendant search that, from a stable anchor, finds the first matching descendant at any depth, tolerating wrapper elements that the rigid path could not. Predicate helpers byIA2Attribute(key, value) (exact match) and byIA2Class(value) (CSS class-token membership).

Firefox address field (firefox.py): extracted addressField(foreground, ffVersion) + thin getURL() as the single home for locating the URL-bearing element. FF≥133 is unified to anchor #urlbar then findInSubtree(#urlbar-input), which subsumes both .urlbar-input-box (133–150) and .urlbar-input-container (151+). Genuinely-different pre-133 trees keep their explicit version paths. Both script_url and the previously-broken event_alert now go through it (the knowledge had drifted into two places; FF151 broke both).

Thunderbird recipients (thunderbird.py): messageHeaderRecipients() replaces rigid paths up to five levels deep (sender) with the same anchored search — the second adapter that proves the shared seam.

Observable failures (shared): when searchObject misses a milestone it now logs which milestone failed and the ids/classes actually present at that level, so the next Mozilla restructure self-reports from a user's NVDA log instead of needing a hand-injected probe.

Tests

Establishes a tests/ harness runnable with plain pytest (no NVDA runtime — it's faked via sys.modules, and the search helpers are pure tree walks over fake NVDAObjects). 20 tests, including a regression anchored on the exact FF151 break and the Thunderbird analogue (survival of an inserted wrapper).

Adds CONTEXT.md with address field and message header recipients as glossary terms.

akj added 2 commits June 4, 2026 13:26
Mozilla restructures Firefox's a11y tree between releases; FF151 inserted a
.urlbar-input-container wrapper that broke the rigid direct-child path behind
NVDA+A (Address not found), and also silently broke the notification
domain-tagging path in event_alert.

- B: add findInSubtree(anchor, predicate) + byIA2Attribute() to shared -- an
  anchored descendant search that tolerates wrapper elements between a stable
  anchor and the target, where searchObject's direct-child path could not.
- A: extract addressField()/getURL() in firefox.py as the single home for
  locating the URL-bearing element. FF>=133 unified to anchor #urlbar then
  findInSubtree(#urlbar-input), subsuming both .urlbar-input-box (133-150) and
  .urlbar-input-container (151+). Rewire script_url AND event_alert to it.
- C: searchObject now logs which milestone missed and the ids/classes present
  at that level, so the next restructure self-reports from a user's NVDA log.

Establish a tests/ harness (plain pytest, NVDA runtime faked via sys.modules)
with FF151/pre-151/pre-133 fixtures, anchored by a regression test for the
exact FF151 break. Add CONTEXT.md with 'address field' as the first term.
The Thunderbird message-header address fields had the same brittleness as the
Firefox urlbar: a rigid sender path up to five levels deep
(headerSenderToolbarContainer -> expandedfromRow -> multi-recipient-row ->
recipients-list -> fromRecipient0) plus 3-level To/CC paths, all matched
direct-child-only -- one inserted wrapper away from breaking exactly like FF151.

Extract messageHeaderRecipients() in thunderbird.py: anchor on the message
header landmark, find #fromRecipient0 at any depth, and each recipient row by id
then its .recipients-list within -- the second adapter for findInSubtree, which
is what proves the shared seam. Add byIA2Class() (CSS class-token membership) to
shared, since recipient lists carry multiple class tokens that a whole-string
match would miss.

Tests cover sender/To/CC location, survival of an inserted wrapper (the
Thunderbird analogue of the FF151 break), class-token matching, and the no-CC /
sender-only headers. Document 'message header recipients' in CONTEXT.md as the
second adapter.
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