Repository navigation
[js][py][rb][java][dotnet] add the accessibility snapshot atom and the binding methods - #18232
AutomatedTester wants to merge 85 commits into
Conversation
First slice of the in-page accessibility snapshot proposed in ADR 18205
(docs/decisions/18205-accessibility-tree-snapshot.md): the atom exists,
ships through @selenium/atoms, and pins the root contract, so the
remaining slices (roles, hidden, tree walk, names, states, composed DOM,
corpus) and the binding PRs can build against a stable subpath.
- registry entry `accessibility-snapshot` in typescript/defs.bzl,
exported as `accessibilitySnapshot` and published under
`@selenium/atoms/accessibility-snapshot`; package.json regenerated
with typescript/generate-exports.cjs
- entry point plus core/accessibility/{types,snapshot}.ts; the
ATOM_SRCS and tsconfig globs now reach subdirectories under core/
- fragment `accessibility-snapshot-typescript` with the same visibility
as the other TypeScript atoms the bindings embed
- QUnit page accessibility_tree_typescript_test.html, written first:
the whole page is rooted at the html element as role `document`
named from the title, Document and html roots are equivalent, every
node carries the full schema with empty description/states, and a
non-element or detached root throws rather than returning a partial
tree
Below the document root every element is still reported as an unnamed
`generic` node; roles, names, states and children arrive in the next
slices, so README.npm.md is not updated yet.
|
/agentic_describe |
|
/agentic_review |
PR Summary by QodoAdd accessibility snapshot atom scaffold and role computation
AI Description
Diagram
High-Level Assessment
Files changed (12)
|
Code Review by Qodo 1.
|
|
/agentic_review |
| const parentElement = parent as Element | ||
| if (parentElement.shadowRoot) { | ||
| return NOT_RENDERED |
There was a problem hiding this comment.
2. Hidden closed-shadow children seem visible 🐞 Bug ≡ Correctness
flatTreeParent() returns the host for its light-DOM child when host.shadowRoot and child.assignedSlot are null, even though both are null for children of a closed shadow root. An unslotted child of such a host consequently reaches the document in isHidden() and returns false; a slotted child can likewise bypass hiding on its inaccessible slot ancestors.
Agent Prompt
## Issue description
The flat-tree traversal silently treats light-DOM children of closed shadow hosts as rendered, although their actual slot assignments cannot be inspected.
## Fix Focus Areas
- javascript/atoms/typescript/core/accessibility/hidden.ts[61-87]
- javascript/atoms/test/accessibility_hidden_typescript_test.html[148-168]
## Recommended Fix
Establish an explicit way to handle inaccessible closed-root assignments rather than returning the ordinary DOM parent as proof of rendering. Add coverage for an unslotted child and a child assigned beneath hidden shadow content.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Not changed. A closed shadow root is invisible to script: host.shadowRoot and child.assignedSlot are both null, and there is no API that distinguishes "no shadow root" from "closed shadow root" without side effects. Treating those children as rendered is the only choice that does not drop light-DOM content that may well be slotted. The limitation is documented in flatTreeParent and in the PR description, and ADR 18205 already lists closed roots as a known edge.
Generated by Claude Code
|
Code review by qodo was updated up to the latest commit cf6f55c |
|
/agentic_review |
da3cf41 to
204a0a8
Compare
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 204a0a8 |
Second slice of ADR 18205: every node now carries the role its explicit `role` attribute names, or the html-aam implicit role. - explicit roles: the first token naming a WAI-ARIA 1.2 role authors may use (abstract roles and unknown tokens fall through), matched ASCII case-insensitively; none/presentation are ignored on focusable elements and on elements with a global ARIA attribute (WAI-ARIA 1.2 section 9.1) - implicit roles: the html-aam table, including the contextual rows (header/footer outside sectioning content, aside and section only when labelled, li only inside ul/ol/menu, select by multiple/size, input by type and list, td as gridcell inside a grid, th by scope, img alt="") - elements html-aam gives no role report `generic`, so the pruning rule in ADR 18205 treats them like a div; frames report the ADR's `iframe` token - two deliberate divergences towards what engines expose, pinned by the test page: input type=password is a textbox and summary is a button accessibility_roles_typescript_test.html was written first and failed 28 of 32 tests against the previous slice; it now passes 125 assertions in Chromium, with the tree page unchanged.
Third slice of ADR 18205: core/accessibility/hidden.ts decides whether an element is hidden in the accname sense, which the tree walk in the next slice uses to leave nodes out. - isHidden: aria-hidden=true on the element or an ancestor (aria-hidden false cannot undo it), display none, visibility hidden or collapse on the element's own computed style, the hidden attribute through the UA stylesheet (so author CSS can show it again), closed details content other than the first summary, noscript, template content, detached elements - ancestry follows the flat tree: a slotted element follows its slot, a shadow root's child its host; an unslotted child of a shadow host and the fallback content of a filled slot are not rendered - hiddenState reports what an element does on its own (rendered, self, subtree) so the walk can prune a subtree once and still descend into a visibility-hidden element whose children may be visible - this is deliberately not bot.dom.isShown: opacity, off-screen position and the clip pattern stay rendered, which the page pins The module is reached through a test-only bundle, typescript/testing/ accessibility.ts, registered in TEST_BUNDLES and never published, since the public atom only exposes hidden-ness through the tree walk. accessibility_hidden_typescript_test.html was written first and failed 15 of 21 tests against a stub; it now passes all 55 assertions in Chromium, with the roles and tree pages unchanged.
Fourth slice of ADR 18205: a snapshot now has children. The walk visits each element once, top-down, threading ancestor state instead of asking isHidden per node. - core/accessibility/tree.ts: an element produces a node when its role is not generic, none or presentation (or, once names land, when it has a name); pruned elements hoist their children in place, so document order is kept; aria-hidden and display none drop the subtree; a visibility hidden element drops its own node and text but descends, since a child may set itself visible again; closed details content is skipped; text runs become `text` leaves with collapsed whitespace and no element; iframe and roles with presentational children (button, img, checkbox, option, ...) are leaves - the root always produces a node, even a generic or hidden one, so a subtree snapshot of a plain div is deterministic; a root hidden as a subtree has no children - core/accessibility/context.ts: per-call SnapshotContext with a role cache and the set of placed elements. It lives exactly as long as the call because the snapshot is a point-in-time copy; a module-level cache would return stale results after the page changes an attribute. Style and name caches join it when the name slice needs them. The walk uses childNodes; the composed-DOM slice swaps in the flat tree and aria-owns. Names below the document root are still empty, so text leaves are not yet suppressed under nodes named from content. accessibility_tree_typescript_test.html gained the walk fixtures first and failed 6 of 17 tests against the previous slice; it now passes all 44 assertions in Chromium, with the roles and hidden pages unchanged.
204a0a8 to
3b8f4dd
Compare
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 3b8f4dd |
Fifth slice of ADR 18205: every node now carries its accessible name and
description, following the accname 1.2 text alternative computation with
the html-aam rules for native elements.
- core/accessibility/accname.ts: aria-labelledby (space-joined, hidden
references included, not followed from a referenced element, a self
reference contributes the control value), aria-label, native sources
(labels with the control itself left out, button input values with
the Submit and Reset defaults, img and area alt, legend, figcaption,
caption, option and optgroup labels, svg title, the document title),
embedded control values, name from content for the roles that allow
it and for every descendant during recursion, with ::before and
::after content included and block-level children set off by spaces,
then title, then placeholder for text inputs
- descriptions: aria-describedby, aria-description, then title when the
name did not already use it
- core/accessibility/value.ts: the values accname step 2E reads from
textboxes, selects and range controls; the states slice reuses it
- core/accessibility/context.ts gains the computed-style and name
caches, which is where repeated work first appears: labelledby
targets are read once per snapshot and descendants keep the style
the walk already resolved
- the walk applies the full pruning rule, so a generic element with a
name now produces a node, and text an ancestor's name was computed
from is not repeated as leaves below it, while the element structure
there still is
Two choices the record leaves open are pinned by the pages: title beats
placeholder on text inputs, as html-aam orders them, and inline children
concatenate without an added space ("Save<b>now</b>" is "Savenow").
accessibility_name_typescript_test.html (29 tests) and
accessibility_description_typescript_test.html (7 tests) were written
first and failed 27 and 6 of them against the previous slice; the tree
page gained the named-generic and consumed-text cases and failed 6 of
19. All five pages now pass in Chromium: 74, 12, 47, 125 and 55
assertions.
|
/agentic_review |
| } | ||
| const node = createNode(role, name.text, computeDescription(element, context), element) | ||
| if (!isLeafRole(role)) { | ||
| collectChildren(element, context, false, textConsumed || name.source === 'content', node.children) |
There was a problem hiding this comment.
23. Text under content-named nodes can disappear 🐞 Bug ≡ Correctness
collectChildren drops every text leaf below a node whose name source is 'content', assuming the text is already in that name. A descendant whose own aria-label or aria-labelledby replaces its text during recursion (e.g. <h1>Intro <span aria-label="X">Y</span></h1>) gets its replacement into the name, while its real text 'Y' is suppressed. That text then appears nowhere in the snapshot.
Agent Prompt
## Issue description
Text leaves are suppressed under content-named nodes even when a descendant's text was replaced by its own aria-label or labelledby in the name.
## Fix Focus Areas
- javascript/atoms/typescript/core/accessibility/tree.ts[115-124]
## Recommended Fix
When descending into an element whose own `computeName` source is 'label' or 'reference', reset `textConsumed` to false for its subtree, because that subtree's text was not used in the ancestor's name.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Not changed, by design. In <h1>Intro <span aria-label="X">Y</span></h1> the heading's name is "Intro X" and the span survives as a named generic node, so "X" is what the tree reports twice and "Y" nowhere. That matches what assistive technology hears: aria-label replaces the span's text in the heading's name and in the span's own name. Repeating "Y" as a text leaf would put text in the snapshot that no user is given. Keeping element structure but not text below a content-named node is one of the choices the PR description lists for confirmation on the ADR thread.
Generated by Claude Code
|
Code review by qodo was updated up to the latest commit cc70c59 |
The browser fixture fetched the corpus expectations from the test web server by blocking on the HttpClient tasks with GetAwaiter().GetResult(). The maintainer asked for an async FetchAsync instead. The expectation readers are now async and awaited from the test, which is async already. The one read that cannot await is the case list, since NUnit discovers test cases synchronously; it uses the client's synchronous Send and parses the index from the response stream, so no task is blocked on.
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit e8706de |
The maintainer asked that members be described with the language's documentation comments rather than line comments, private ones too. Every line comment that sat above a member of the snapshotter, the node, the driver's BiDi members and the browser fixture is now a <summary>, and the private helpers that had no comment get a one-line summary. No code changes.
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 0c0dcdf |
The test that a driver fails to construct when its WebSocket URL cannot be reached sat in the accessibility snapshot unit tests, where the maintainer pointed out it does not belong: it checks the driver's BiDi connection, not snapshots. It moves to WebDriverConnectionTests in the BiDi unit test folder, and the snapshot fixture keeps only the two tests about snapshots without BiDi, with a helper that no longer takes a WebSocket URL.
|
/agentic_review |
| /// NUnit discovers test cases synchronously, so the case list is the one corpus read that cannot | ||
| /// await; it uses the client's synchronous Send rather than blocking on a task. | ||
| /// </summary> | ||
| public static IEnumerable<string> ListCorpusPages() |
There was a problem hiding this comment.
IMHO it will be better to explicitly enumerate well-known pages instead of parametrized tests relying on WebServer in runtime.
Discussable. Needs to be aligned with all bindings.
There was a problem hiding this comment.
Agreed it is a cross-binding call, so I have not changed it here and am leaving the thread open for @AutomatedTester. Where the five bindings stand: Python, JavaScript, Ruby and Java all parametrize their corpus test from index.json, read from the checkout at collection time, so a page added to the corpus is picked up everywhere without touching any binding; .NET is the only one reading the index over HTTP, and only because the browser suite was asked not to read the source tree. If the choice is an explicit list, the shape I would propose for all five is the same: the page names written out in the test, plus one test that asserts the written list equals the corpus index.json, so a new page fails loudly instead of being silently skipped. Happy to apply that to all bindings in a follow-up commit once the author says which way to go.
Generated by Claude Code
There was a problem hiding this comment.
True. We should discuss. My concern is that test should never rely on some "unpredictable" file path (index.json) in this case. This is why I proposed to get it from WebServer since it is already served properly.
To highlight the issue: in .NET it is possible to (pseudo):
dotnet build --output "ftp://something"
cd "ftp://something"
dotnet run # execute testsindex.json file is not part of build. Agent, please see how we deal with shared web extensions to test it as aprt of build. Summarize.
There was a problem hiding this comment.
Summary of how the shared web extensions reach the .NET tests as part of the build, and what it means for the corpus:
Two build paths, both ship the shared files with the test output.
- Bazel:
//common/extensionsis afilegrouplisted indataof thedotnet_nunit_test_suiteindotnet/test/webdriver/BUILD.bazel, so the files are in the test's runfiles. The web server library (//dotnet/test/testing.webserver) lists//common/src/webthe same way. - csproj:
Selenium.WebDriver.Tests.csprojlinks..\..\..\common\extensions\**as<None>items withCopyToOutputDirectory=PreserveNewestundercommon\extensions\..., sodotnet build --output Xcopies them intoX/common/extensions/.Selenium.Testing.WebServer.csprojdoes the same forcommon\src\web\**, so the wholecommon/src/webtree, the accessibility corpus and itsindex.jsonincluded, lands inX/common/src/web/as part of the build.
How the tests find them at run time.
WebExtensionTests.LocateRelativePathandFirefoxDriverTests.GetPathcallBazel.Runfiles.Create().Rlocation("_main/common/extensions/...")(theRunfilesNuGet package, referenced by both the Bazel target and the csproj) and fall back to a path relative to the assembly's base directory when there is no runfiles manifest (plaindotnet test).AppServer.FindWebContentRootwalks up fromAppContext.BaseDirectoryuntil it findscommon/src/web, so underdotnet build --output Xit servesX/common/src/web, and under Bazel the runfiles copy.EnvironmentManagerresolves configured paths the same way, throughTEST_SRCDIR/_mainwhen set.
Implication for this fixture. Because the web server project already carries common/src/web into the build output, the index.json the fixture reads over HTTP is the one from the build output, not from the source checkout: in your dotnet build --output "ftp://something" scenario it is served from .../common/src/web/accessibility/index.json inside that output. So the HTTP read is already "part of the build" in the same sense the extensions are. If reading it from disk is preferred, the extension pattern applies directly: link common\src\web\accessibility\index.json (or the whole corpus) into the test csproj and resolve it with Runfiles.Rlocation with the base-directory fallback, no new mechanism needed. The explicit list in the test plus a check against the served or linked index.json remains the third option. I have changed nothing pending the discussion and the author's call, since the other four bindings parametrize from index.json on disk and should move together.
Generated by Claude Code
|
Code review by qodo was updated up to the latest commit 7e06a36 |
The test that a driver fails to construct when its WebSocket URL cannot be reached accepted any exception, so an unrelated constructor failure would have passed it. It now asserts the WebSocketException the refused connection raises, as the inner exception of the invocation exception the mocked driver's proxy wraps it in, and its name says so.
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit fb18d41 |
The maintainer asked that classes new in this PR follow the newer .NET conventions rather than the older ones the existing classes still mix: no `this.` when accessing members, private instance fields named `_camelCase` and private static fields `s_camelCase`. The snapshotter, the node and the browser fixture drop every `this.` qualifier, the snapshotter's three fields and logger and the node's state order are renamed, and the classes that are not new keep their existing style. No behaviour changes.
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit ca13798 |
The maintainer asked that the driver's field holding the lazily created AccessibilitySnapshotter be named after that class rather than "accessibility". No behaviour change.
|
/agentic_review |
The helper's XML comment was copied from GetElementFromResponse and still described a "response" parameter the method does not have, which the compiler reported as CS1572 on every .NET build. The comment now describes the method and its elementId parameter. The other warning the build reported, a nullable annotation in the snapshot unit tests, went away when that test moved in 7e06a36. No code changes.
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 8fa4696 |
|
I missed ADR or discussion of public API. This PR proposed flatten API: Hm. may be GetAccessibilityTree? |
|
@nvborisenko the public API is decided in ADR 18205 (SeleniumHQ/selenium#18205, On "Snapshot" versus "Tree": the ADR's title is "Accessibility tree snapshots", and the word is deliberate. The result is a computed, point-in-time copy of the tree that does not update with the page and is explicitly not the browser's native accessibility tree, and the name matches what users know from Playwright's One .NET detail worth your eye: the ADR text says .NET would use extension methods to avoid new interface members. This PR instead adds Generated by Claude Code |
|
If And this is a concern: why we need Agent, don't react on this comment. |
🔗 Related Issues
docs/decisions/18205-accessibility-tree-snapshot.md), the shared corpus the record makes the cross-binding contract, and the binding adoptions (Python, JavaScript, Ruby, Java, .NET). The ADR is still Proposed; this PR implements it as written there, and the open decisions listed under Additional Considerations are the ones to settle on that record or here.💥 What does this PR do?
Adds the in-page JavaScript that computes an accessibility snapshot, as a TypeScript atom in
@selenium/atoms, plus the shared test corpus, and the Python, JavaScript, Ruby, Java and .NETaccessibility_snapshot()/getAccessibilitySnapshot()/accessibility_snapshot/GetAccessibilitySnapshotAsync()methods on top of it. One commit per slice, each with its tests written before the implementation:Scaffold (
4a4bf54)accessibility-snapshotintypescript/defs.bzl, exported from the barrel asaccessibilitySnapshotand published under@selenium/atoms/accessibility-snapshot.package.jsonwas regenerated withtypescript/generate-exports.cjs, so//javascript/atoms:exports-up-to-datekeeps it honest.typescript/accessibility-snapshot.ts(re-export only) pluscore/accessibility/types.ts(the ADR §2AccessibilityNodeshape, also exported as a type) andcore/accessibility/snapshot.ts.//javascript/atoms/fragments:accessibility-snapshot-typescriptwith the same visibility as the other TypeScript atoms the bindings embed.Roles (
bac5a29)core/accessibility/roles.ts: the explicitroleattribute (first token naming a WAI-ARIA 1.2 role authors may use, matched ASCII case-insensitively, abstract and unknown tokens fall through;none/presentationignored on elements that can take focus and ones with a global ARIA attribute, per WAI-ARIA 1.2 §9.1), then the html-aam implicit mapping including its contextual rows (header/footer outside sectioning content, aside, section and form only when named,lionly inside a list, select by multiple/size, input by type andlist,tdas gridcell inside a grid,thby scope,img alt=""as presentation, required owned elements of a presentational table or list as generic).accessibility_roles_typescript_test.html: 35 tests, 138 assertions.Hidden-ness (
0b99154)core/accessibility/hidden.ts:isHiddenin the accname sense.aria-hidden=trueon the element or an ancestor (falsecannot undo it),inert,display: none,visibility: hidden/collapseon the element's own computed style,content-visibility: hiddenfor everything below the element, thehiddenattribute via the UA stylesheet (so author CSS can show it again), closeddetailscontent other than the firstsummary,noscript, template content and detached elements. Anareais as hidden as the image whoseusemappoints at its map. Ancestry follows the flat tree.hiddenStatereports what an element does on its own (rendered,self,contents,subtree) so the walk can prune once and still descend into a visibility-hidden element whose children may be visible.accessibility_hidden_typescript_test.html: 24 tests, 65 assertions, including the cases that pin this as notbot.dom.isShown.Tree walk (
3b8f4dd)core/accessibility/tree.ts: each element is visited once, top-down, with ancestor state threaded down. An element produces a node when its role is notgeneric,noneorpresentationor when it has a name; pruned elements hoist their children in place. Subtree-hidden elements are dropped; a visibility-hidden element drops its own node and text but descends; a contents-hidden element keeps its node and drops its children; closeddetailscontent is skipped. Text runs becometextleaves with collapsed whitespace and noelementkey. Frames, textareas, video and audio are leaves by element; roles with presentational children are leaves by role.core/accessibility/context.ts: a per-callSnapshotContext, alive exactly as long as the call because the snapshot is a point-in-time copy.accessibility_tree_typescript_test.html: 30 tests, 60 assertions.Names and descriptions (
cc70c59)core/accessibility/accname.ts: the accname 1.2 text alternative computation with the html-aam element rules (aria-labelledby,aria-label, embedded control values, native sources, name from content with generated content and block spacing,title,placeholder); descriptions fromaria-describedby,aria-description, thentitlewhen the name did not use it.core/accessibility/value.tsholds the embedded-control values.accessibility_name_typescript_test.html(now 38 tests) andaccessibility_description_typescript_test.html(7 tests, 12 assertions).Review fixes (
6c5e36b): the Qodo findings on the slices above, each with a regression case added first; the two not changed are answered on their threads.States and values (
1f42b8c)core/accessibility/states.ts:checked,pressed,expanded,selected,level,disabled,focusedper the ADR §2 table (see Implementation Notes for the two readings of "whenever the role supports it");valuefor textbox, searchbox, combobox and range roles.README.npm.mddocuments the atom.accessibility_states_typescript_test.html(18 tests, 61 assertions) andaccessibility_value_typescript_test.html(6 tests, 24 assertions).Composed DOM and aria-owns (
3014e9a)aria-ownsre-parents: owned elements leave their DOM position and follow the owner's own children, so accessibility parentage wins (ADR §3) while the element reference still identifies the element.accessibility_shadow_typescript_test.html(10 tests) andaccessibility_owns_typescript_test.html(9 tests).Shared corpus (
80d61c4)common/src/web/accessibility/: 16 plain pages covering the areas above, each with the whole-document snapshot recorded as.expected.json(element references written as ids) and as.expected.txtin the ADR §1 text form;index.jsonlists them andREADME.mdgives the conventions, how to record, and where engine divergences go. This corpus, not any binding's tests, is the cross-binding contract (ADR §4).typescript/testing/accessibility-text.ts: the referencetoText(- role "name" [state] [state=value]: value, two-space indent,- text: contentleaves), in the test-only bundle so it is never published; the bindings port it and the corpus.txtfiles hold the ports to one source.typescript/accessibility-node.schema.json: the ADR §2 shape as JSON Schema with the states table spelled out.accessibility_corpus_typescript_test.htmlloads each page in an iframe and compares tree and text with the expectation, preferring an engine-specific file when one is recorded;?record=1shows the expectations to record instead.//javascript/atoms:accessibility-unit-tests(the renderer, and a 64 KiB ceiling on the atom bundle) and//javascript/atoms:accessibility-corpus-consistent(every page has both expectation files, every expectation validates against the schema, every.txtis the renderer's output over its.json, ids are unique, the root is thehtmlelement). The second is what turns the ADR's compatibility rule into a failing test.Hardening (
961f924)accessibility_robustness_typescript_test.html: a 5000-node subtree, a 1000-level deep tree, 500 cells labelled by one header, an owner of 500 elements, references to nothing, odd role tokens, duplicate ids, non-ASCII text, out-of-rangearia-level, and two snapshots of one tree being identical. Interference from page scripts is deliberately not tested: in the same realm the page's scripts run first and can replace any prototype the atom relies on; the bindings install the atom in a BiDi sandbox realm, which is the design's answer (ADR §4).Performance (
6764d82tod387d80, six commits)test/accessibility_bench.html(not a test: the suite runs only*_test.html) times the atom on fixed tree shapes and counts the DOM calls it makes, as the before/after for this and for the bindings;README.mdrecords how to run it and the baseline.Five per-call caches and one threaded flag, each committed with its own before/after numbers and each gated on every page, the corpus and the Node tests passing unchanged: hidden state cached in the context and read through the shared style cache; generated content (
::before/::after) read once per element and skipped entirely when no stylesheet in the element's scope (adopted sheets,:host::beforeand::slotted()::beforeincluded) can define any, with an unreadable sheet counting as defining it; the text alternative of an element as a descendant of something being named, and its name-from-content text, cached for the standard traversal; the text alternative of anaria-labelledby/aria-describedbytarget cached per target; andaria-disabledthreaded down the walk instead of climbing every ancestor of every node.Measured in headless Chromium 141 (bench page, times vary about 20% between runs, the call counts are the reliable columns), baseline → after:
getComputedStyle/ elementgetAttribute/ elementThe robustness page's fan-in test used to claim the shared header was "resolved once per snapshot" when it was not; it now holds a probe whose attribute reads are counted, and the claim is true.
Two findings of the review pinned as limitations: UA quotes on
qare the keywordopen-quotein computedcontent, not a string, so the atom never read them (names page); and a descendant under a single content-named ancestor is computed once as a descendant and once at top level, two modes no cache can merge.Code review fixes (
711e817to0bfdc6d, nine commits), each with its regression test added first and all recorded expectations unchanged:aria-owns: two siblings owning each other were both claimed and both vanished; an owner with presentational children (a button owning a menu) claimed a target it could never emit; an owned element inside a hidden container was placed as rendered. An element is now owned by the first owner in document order that can take it, a relation that would make the tree circular or whose owner has presentational children is dropped, and owned elements are checked against their DOM ancestors' hidden state.content: url(...)with the URL quoted, so icon pseudo-elements named buttons by their image URL; functions are now read as one token. Adisplay: noneorvisibility: hiddenpseudo-element contributes nothing.::part()::beforefrom the host's scope is now consulted by the gate that skips pseudo-element reads.aria-labelledby: the visited set was shared by a whole name computation, so two cells of a row pointing at one header contributed it once; it is now per attribute (an id listed twice still counts once, as Chromium reads it).focusedinside shadow roots: the document'sactiveElementis the host; the element's own root is asked now, and a host whose open shadow root holds the focus is not itself focused.labelelements ahead ofvalue(html-aam);type=numbertakes a placeholder.header/footerare generic inside an element with thearticle,complementary,main,navigationorregionrole, not only inside those elements.\n,\rand\in values, andaria-valuetextis trimmed likearia-valuenow..gitattributesforces LF on the corpus.txt/.jsonfiles so the byte-exact consistency test holds on a CRLF checkout.CI fixes and the Qodo findings on the later slices (
5e57918,4b2709eto291467d):srcs, and esbuild under Bazel resolves imports only among the sources it is given, so the accessibility test bundle's import of its renderer failed and took the two Node tests and the threeclosure-testtargets with it from the corpus commit on (the local esbuild stand-in resolved from disk and hid it). Every test-only module is now a source of every test bundle. The corpus consistency test took//common/src/webas data, source files from another package that rules_js refuses to copy into ajs_test; the corpus now comes through ajs_libraryin its own package.//common/src/webis written in the short form buildifier wants.aria-ownsowners are found along the flat tree, so an owner inside an open shadow root re-parents within its tree and a light-DOM owner no slot renders claims nothing (it used to take a rendered target out of the snapshot).disabled: an ancestor'saria-disabledreaches the descendants that can take focus (WAI-ARIA 1.2), not the headings and paragraphs between them, and the ancestor check used for a root or owned element climbs through shadow hosts.aria-pressed,aria-expandedand the grid parts'aria-selectedset toundefinedor empty count as absent. An option element given another role does not report the native selection.valueis masked, one bullet per character, as the browser's own tree exposes it, both as the node's value and for what the field contributes to an enclosing name; corpus page 004 is re-recorded.slotelement is treated as a slot; an SVG element namedslotno longer aborts the snapshot.:host::beforerule in a closed root's sheet names the host only while some sheet in scope defines generated content, because the gate that skips pseudo-element reads cannot see a closed root's sheets. One finding did not reproduce (bmaps togeneric, as the robustness test asserts; it passes in all three engines).Python binding (
b974453), the first binding slice:driver.accessibility_snapshot()andelement.accessibility_snapshot(), both returningAccessibilityNode(role,name,description,value,states,element,children) withto_text(), inselenium/webdriver/remote/accessibility_snapshot.py.script.addPreloadScriptinstalls it into theseleniumsandbox realm of every new document, and a document that predates the preload installs it on first use in onecallFunction. Results come back withmaxDomDepth: 0, so every element is anoderemote value whosesharedIdbecomes aWebElement.iframenode is filled in with the tree of its browsing context, found through the frame'scontentWindow(awindowremote value carries the context id), so cross-origin frames stitch too; a frame that cannot be reached stays a leaf. The whole-page snapshot is rooted at the top-level context of the current window whatever frame the driver has switched to.StaleElementReferenceExceptionis raised. A session without BiDi raisesWebDriverExceptionnaming BiDi, from the accessor both entry points go through.//py:unitholdsto_text()to every corpus page's recorded text (the port is held to the reference renderer by the same files the atom is), the line grammar's escapes, the wire decoder and the preload retry; the BiDi suite snapshots every corpus page whole and compares tree and text (element references mapped to ids, an engine-specific expectation preferred where one exists), then covers the document root, references usable for further commands, subtree roots, frames (including a snapshot taken while switched into a frame, a subtree inside one and an iframe as the root), stale and detached roots, and a snapshot being a copy; the classic suite checks both methods raise. Locally, with the generatedbidipackage, pytest and ChromeDriver 141 against headless Chromium 141: 20 unit, 26 BiDi and 2 classic tests pass; ruff and mypy are clean.JavaScript binding (
5d8aaff), the second binding slice, the same design inlib/accessibility_snapshot.js:await driver.getAccessibilitySnapshot()andawait element.getAccessibilitySnapshot(), both resolving to anAccessibilityNodewithtoText(); the class is exported from the package. The atom module built bylib/atoms/make-atoms-module.js(newaccessibility_snapshottarget, shipped in the npm package) is a function whose source is the function declarationscript.callFunctiontakes, so nothing is stringified by hand. Preload into theseleniumsandbox realm, on-demand install,maxDomDepth: 0, frame stitching throughcontentWindow, the per-context search for a subtree root,StaleElementReferenceErrorfor stale and detached roots, andUnsupportedOperationErrornaming BiDi for a session without it, as in Python.small-testsholdstoText()to every corpus page's recorded text, the line grammar's escapes, the wire decoder, the BiDi guard and the preload retry; a large test on the BiDi browsers (Chrome, Edge, Firefox) mirrors the Python suite over the corpus and the frame, stale, detached and copy cases. Locally, with the generated BiDi modules compiled from the pinned CDDL, mocha and ChromeDriver 141 against headless Chromium 141: 22 small and 26 large tests pass; eslint and prettier are clean.Firefox and the binding review round (
5786c38,c208a48,60c5424,3118f8f,23731ef,f628e56)NodeCache.getNodecompares group ids), so on Firefox a subtree snapshot rooted inside a child frame ran against the top document and CI failed ontest_subtree_snapshot_inside_a_frame. The called script now checksroot.ownerDocument === documentand answersnullfor a root it does not own, which the bindings treat like an unknown node and move on to the next context; the inside-a-frame tests assert the root is the frame's body.browsingContext.getTreepropagates;local_devcopies the Python atom. The unit collection finding did not reproduce (//py:unitcarries//common/src/web, whose glob covers the corpus, and passed on RBE).<iframe role="application">) was reported under that role and so never stitched; the atom now reports every frame with the ADR'siframetoken whatever role attribute it carries (ARIA in HTML allows application, document, img and the presentational pair, and the embedded document is exposed in every case). The JavaScript browser suite names Chrome, Edge and Firefox, the browsers the test environment enables BiDi for, so a macOS run no longer tries Safari.//javascript/selenium-webdriver:eslint-testchecks that every staticrequireresolves in its runfiles, andlib/accessibility_snapshot.jsrequires the built atom module directly wherelib/http.jsloads its atoms through a run-time path the rule cannot follow. The module is now in the lint target's data, and ignored by eslint like the generated BiDi modules, since prettier rejects a generated bundle line by line.Ruby binding (
bf7e235,07d6cbf,102af37), the third binding slice, the same design inlib/selenium/webdriver/common/accessibility_snapshot.rb:driver.accessibility_snapshotandelement.accessibility_snapshot, both returning anAccessibilityNode(role,name,description,value,states,element,children) with#to_text; the bridge holds theAccessibilitySnapshotter(@api private) that does the work. The atom is copied into the gem like the other atoms (//rb/lib/selenium/webdriver/atoms:accessibility-snapshot, read throughAtoms#read_atom). Preload into theseleniumsandbox realm, on-demand install,maxDomDepth: 0, frame stitching throughcontentWindow, the per-context search for a subtree root with the ownership check,StaleElementReferenceErrorfor stale and detached roots, andUnsupportedOperationErrornamingweb_socket_urlfor a session without BiDi, as in Python and JavaScript.BiDi#send_cmddirectly, as the hand-writtenBiDi::BrowsingContextdoes, so node remote values come back as plain hashes and the typed protocol layer's record validation never runs per element. The BiDino such nodeerror and the script's null answer for a root another document owns are raised and rescued as the snapshotter's ownNotOwnedError(aWebDriverError);07d6cbfhad made the module require the BiDi error registry forError::NoSuchNodeErrorinstead, which broke every spec target that depends on the common library alone (no BiDi file in its runfiles) and the unit target's load order, so102af37keeps the class in common.stateskeys are strings, as the wire and the corpus JSON spell them. Signatures inrb/sigcover the new class and the three new methods; steep reports no errors on the new file (two warnings about untyped empty literals, which the codebase does not annotate anywhere).#to_textto every corpus page's recorded text, the line grammar's escapes and the wire decoder, checks the classic bridge raises, and runs the context search and the preload retry over a stand-in connection (the top-level document refuses the root, the child owns it; no owner is the stale error);//rb/spec/unit:unitnow carries//common/src/web. The BiDi spec (//rb/spec/integration/selenium/webdriver/bidi:accessibility_snapshot-*-bidi, generated for Chrome, Edge and Firefox like the other BiDi-only specs) mirrors the Python suite over the corpus and the document root, element references, subtree roots, frames, stale and detached roots, a snapshot being a copy, and the classic guard throughreset_driver!(web_socket_url: false). Corpus files are read as UTF-8 explicitly, since the suite's locale cannot be assumed. Locally, with the bundle and ChromeDriver 141 against headless Chromium 141: 24 unit and 27 integration examples pass; rubocop is clean.Qodo on the atom after the Ruby slice (
d5c5ed1,3ef8abe)formelement the form landmark only when it has an accessible name; the atom gave it to every form. An unnamed form is nowgeneric(through the samehasAuthorNamechecksectionuses), so it is pruned and its controls take its place; corpus pages 001 and 004 are re-recorded, and all three bindings' corpus suites pass on the new expectations. The roles page pins a named, a titled and an unnamed form.aria-label, so a label holding only an image withaltnamed nothing; it now also countsaria-label,alt,titleorvalueon the label or any element below it, the attributes accname reads for a referenced label's descendants. Pinned for a form and a section labelled by an element holding only an image.tabindexcannot take focus, but the focus check read the tabindex first and so refused the presentational role on such a button; native controls are now checked for being disabled before the tabindex is read.aria-ownsowner takes its owned elements with it (pinned since the aria-owns slice), because the owner is the accessibility parent and a hidden parent hides its children; the other reading, dropping the relation for a pruned owner, would leave the element where the author moved it away from. The Python line-width finding did not reproduce (ruff format --checkpasses, as does the Format job).Author review (
e253d8a,dddfee4,f69b13b,7944e7e)WebDriver._accessibility(), the accessor both entry points go through, as Ruby already guards in the bridge.script.addPreloadScriptsucceeds; it used to be set before the call, so a failed preload looked installed and was never tried again. Each binding has a unit test running the preload against a stand-in that fails until told otherwise. In JavaScript, where snapshots can overlap, the registration is one shared promise so two early snapshots do not each register a script.Java binding (
d7f96a1,9390590,adb839d), the fourth binding slice, the same design inorg.openqa.selenium.remote.AccessibilitySnapshotter:WebDriverandWebElementgaindefault AccessibilityNode getAccessibilitySnapshot()throwingUnsupportedOperationException, the patterngetAriaRole()andgetAccessibleName()set, so third-party implementations keep compiling.RemoteWebDriverandRemoteWebElementoverride it; the driver's package-privateaccessibility()accessor is the one place both entry points go through, and it throwsUnsupportedCommandExceptionnamingoptions.enableBiDi()for a classic-only session before any command is sent.org.openqa.selenium.AccessibilityNodeis an immutable value in the core package (getRole(),getName(),getDescription(),getValue(),getStates(),getElement(),getChildren()) withtoText(); states areBoolean,LongorStringunder their wire names, as in the other bindings.getAttribute.jsandisDisplayed.js(copy_fileof the fragment, listed as a resource) and runs over the existingbidi.module.Script: preload into theseleniumsandbox realm, on-demand install,maxDomDepth: 0,ResultOwnership.NONE, frame stitching throughcontentWindow(WindowProxyProperties.getBrowsingContext()), the per-context search for a subtree root with the ownership check (browsingContext.getTreethroughBiDi.sendwith a plain map result, since the remote package cannot depend onbidi/browsingcontextwithout a cycle),StaleElementReferenceExceptionfor stale and detached roots. Node remote values becomeRemoteWebElements through the driver's element converter, so they are usable for further commands like any found element. The BiDino such nodeerror, which arrives as aWebDriverExceptioncarrying the raw error JSON, is matched on that text; there is no typed exception for it in the Java BiDi layer yet.//java/test/org/openqa/selenium:SmallTestsgainsAccessibilityNodeTest, holdingtoText()to every corpus page's recorded text plus the line grammar's escapes, the indentation and the read-only views (the suite now takes//common/src/webas data, whose filegroup is now visible to the package, and//java/test/org/openqa/selenium/buildforInProject);//java/test/org/openqa/selenium/remote:small-testsgainsAccessibilitySnapshotUnitTest, a classic-onlyRemoteWebDriverover a recording executor that asserts both methods raiseUnsupportedCommandExceptionand send nothing;//java/test/org/openqa/selenium/bidi:AccessibilitySnapshotTest(and the-firefox,-edge,-chrome-beta,-firefox-betavariants) mirrors the Python suite over the corpus and the document root, element references, subtree roots, frames, stale and detached roots and a snapshot being a copy. Both small suites and the bidi suite now depend onjunit-jupiter-params. Locally, the changed and new sources compiled withjavac --release 11against the released 4.51.0 jars, the two small test classes ran under the JUnit console launcher (21 tests), and a harness carrying the bidi test's assertions ran against ChromeDriver 141 and headless Chromium 141 (46 checks); google-java-format 1.37.0 was applied.9390590reorders one dependency the Format job's buildifier wanted sorted;adb839dmakes//common/src/webvisible to the core test package, which RBE's analysis required.Qodo after the Java slice (
189d055,e9e1a3d)attr()in a pseudo-element's computedcontentto its string before serialising it, but Firefox keeps the function, so a button named bycontent: attr(data-label)lost that label there. The parser now reads the named attribute from the originating element for anattr()token (name only) and still treatsurl()and the other functions as not text; the names page pins a present and a missing attribute.Remote::Bridge.element_class, as every other lookup does, so a configured custom element class applies to them; the unit spec snapshots with a custom class..NET binding (
84859e7,3cbbed5,9868337), the fifth binding slice, the same design inOpenQA.Selenium.AccessibilitySnapshotter:WebDriver.GetAccessibilitySnapshotAsync()andWebElement.GetAccessibilitySnapshotAsync(), bothTask<AccessibilityNode>with aCancellationToken, asvirtualmethods on the classes rather than members ofIWebDriver/IWebElement, the wayComputedAccessibleLabelandComputedAccessibleRoleare exposed, so third-party interface implementations keep compiling; the API is async because the .NET BiDi layer is. The driver's internal accessor is the one place both entry points go through, and it throwsUnsupportedOperationExceptionnamingoptions.UseWebSocketUrlwhen the session was started without BiDi, before any command is sent.OpenQA.Selenium.AccessibilityNodeis an immutable sealed class (Role,Name,Description,Value,StatesasIReadOnlyDictionary<string, object>ofbool,longorstring,ElementasIWebElement?,Children) withToText().generated_resource_utilitieslike the other three atoms (ResourceUtilities.AccessibilitySnapshotAtom). As first written the snapshotter opened its own BiDi connection withAsBiDiAsync()on the first snapshot and closed it with the driver; the review round below moves that connection intoWebDriver. Over it: preload into theseleniumsandbox realm (AddPreloadScriptOptions.Sandbox), on-demand install,MaxDomDepth = 0,ResultOwnership.None, frame stitching throughcontentWindow(WindowProxyRemoteValue.Value.Context), the per-context search for a subtree root with the ownership check (BrowsingContext.GetTreeAsyncrooted at the window),StaleElementReferenceExceptionfor stale and detached roots. Node remote values becomeWebElements through the driver'sWebElementFactory, so a configured custom element factory applies. The BiDino such nodeerror arrives as aBiDiExceptionwhose message starts with the error code and is matched on that text, as in Java.3cbbed5): disposal takes the connection lock, so a first snapshot still connecting when the driver is disposed cannot leave its connection open and a later snapshot fails withObjectDisposedExceptionrather than reconnecting; a failure while closing the connection is logged rather than thrown, so the driver's own session quit and executor disposal always run.9868337drops an unusedusingthe Format job'sdotnet format styleremoved.//dotnet/test/unit/webdriver:unitgainsAccessibilityNodeUnitTests(the line grammar's escapes, indentation, read-only copies, argument checks) andAccessibilitySnapshotUnitTests(both methods raiseUnsupportedOperationExceptionon a session without BiDi; see the review round below for their current shape); perdotnet/TESTING.mdthe unit project stays off the filesystem, so the corpus text comparison lives in the browser suite.//dotnet/test/webdriver:AccessibilitySnapshotTests(and its browser variants) mirrors the Python suite over the corpus (tree compared withJsonNode.DeepEquals, text byte for byte, an engine-specific expectation preferred where one exists) and the document root, element references, subtree roots, frames, stale and detached roots and a snapshot being a copy. Locally, with a .NET 10 SDK: the WebDriver sources (minus the generated DevTools protocol, which needs Bazel) compiled with the new files, the unit fixtures ran under NUnit, the browser fixture compiled against stand-ins for the test environment, a harness carrying its assertions ran against ChromeDriver 141 and headless Chromium 141 (46 checks), anddotnet format whitespaceandstylereport no changes.Wire shape of the atom (
ab4dbc8; then7c8e434Python,ffb506aJavaScript,7664cc3Ruby,fa48ac2Java,dbb6be9.NET;e306305on Firefox), the simplification round that moves binding logic into the atom:accessibility-snapshot-wire(core/accessibility/wire.ts, exported asaccessibilitySnapshotWire, fragmentaccessibility-snapshot-wire-typescript, published under@selenium/atoms/accessibility-snapshot-wire), answers in the one shape a binding needs:{ json, elements, windows }, the tree as one JSON string whose nodes carryelementas an index intoelementsand, on an iframe node whose frame has a content window,frameas an index intowindows;nullfor a root this document does not own;{ error: 'detached' }for a root no longer in its document. So a binding parses one string with its own JSON library and reads the two arrays, which BiDi serialises as node references and browsing context ids in one pass, instead of decoding a nested remote value and asking each frame for itscontentWindow. The pureencodeWireis unit tested without a DOM;accessibility_wire_typescript_test.html(8 tests) pins the shape against the plain atom's tree, first-mention index order, the frame pointer and the data answers; the corpus page decodes the wire form of every page too; the Node tests hold the wire bundle to the same 64 KiB ceiling (51,647 bytes). The plainaccessibility-snapshotentry is unchanged and stays published for in-page use.(root) => (ATOM)(root), once per document. Gone from every binding: the recursive remote-value decoder, the secondcallFunctionper frame, the ownership check written into the declaration, thedetachedmatch on an exception's text, and the preload with its flag, its retry and the on-demand install (addPreloadScriptis no longer used and no state is kept per realm; the only cost is the atom's source on each call, which the browser parses once per document). Kept: the BiDino such nodematch (still an error, not data, until the typed exceptions exist), the per-context search, frame stitching and the BiDi guards. The Python, JavaScript and Ruby unit tests now cover the wire decoder (indices resolved, absent fields absent, frame children through the callback) and their stand-in connections answer the wire shape; Java gains twofromWiretests; .NET's decoder stays private (the unit project has no internals access) and is covered by the browser suite. The five binding corpus suites pass unchanged on the new entry (Python 20 unit and 26 BiDi, JavaScript 24 small and 26 browser, Ruby 25 unit and 27 integration, Java 4 unit and 46 harness checks, .NET 6 unit and 46 harness checks, on ChromeDriver 141 and headless Chromium 141), as do ruff, mypy, eslint, prettier, rubocop, steep, google-java-format anddotnet format whitespace.e306305): Firefox hands a shared id of another document's node to the called function asnull, and the wire entry readnullas "no root" and snapshotted the whole document, so RBE failed every binding's Firefox suite on the subtree-inside-a-frame case. Anullroot now answersnull, the same as a root another document owns, so the bindings move on to the next browsing context; only an absent root means the whole document. The wire page pins both answers.to_text()either a second call or a second payload on every snapshot; held for the ADR thread..NET review by @nvborisenko (
44b04be,4ce3934,19da7a3,2c47192,81c9b20,1fc7f71,55c5148,67ccb84,5caaf88,1b28d31,3554a3c,6ff78ca,baad8a3,482a22a,e8706de,0c0dcdf,7e06a36,fb18d41,ca13798,2feeea9,8fa4696)WebDriverowns the BiDi connection: a session whose capabilities carry a WebSocket URL connects in the constructor, right after the session is created, so every internal component that works over BiDi shares oneIBiDi(the internalBiDiproperty) for the driver's life, nothing is opened lazily and no connection lock or close flag is needed; the driver closes it fromDispose/DisposeAsyncCore. A connection that cannot be made fails the constructor, which quits the session it just started;BiDi/WebDriverConnectionTestspins that with a WebSocket URL nothing listens on, asserting theWebSocketExceptionthe refused connection raises (moved out of the snapshot fixture in7e06a36, since it is about the connection, not snapshots; the exact exception asserted infb18d41). The driver's field for the lazily created snapshotter is named after its class (2feeea9). The publicAsBiDiAsync()extension is unchanged and keeps opening a connection its caller owns, by the maintainer's decision; internal components use the shared one, which connects with default options (propagating top-level options downstream is future work, also by the maintainer's decision). The constructor documents this (482a22a), and the two snapshot methods' exception docs now say theUnsupportedOperationExceptionis for a session started withoutUseWebSocketUrl, since a session that asked for BiDi and could not connect fails in the constructor instead.AccessibilitySnapshotterhas no reference toWebDriver: it takes theIBiDi, an action giving the current window handle and an element factory, is stateless, and the driver holds one instance in aLazy<T>, created on first use and dropped with the connection on disposal. The BiDino such nodeerror is read as anullanswer (only for a call that sent a shared id), the same the atom gives for a root another document owns, so the context search reads one value and the internal exception type that carried it is gone. Every private helper is named with a verb (ReadContext,GetRequired,FormatLine,ListRoles, ...), every member of the .NET files in this PR, private helpers included, is described with a<summary>rather than a line comment (0c0dcdf), and the classes new in this PR follow the newer member style the maintainer asked for: nothis.qualifiers, private instance fields_camelCase, private static fieldss_camelCase(ca13798;WebDriverandWebElementkeep the style around the lines added to them).8fa4696): the XML comment onWebDriver.CreateElementFromReference, copied fromGetElementFromResponse, named a parameter the method does not have (CS1572), and the one nullable annotation in the unit tests (CS8632) went with the test that moved in7e06a36. The.NET / Unit Testsand.NET / Browser Testslogs on8fa4696report no compiler or analyzer warnings.DriverTestFixture, settingUseWebSocketUrlinConfigureDriverOptions, so one browser serves every test; the corpus expectations are fetched from the test web server overHttpClient, as the pages are (index.jsonfor the case list, then<page>.expected.jsonand.txt, the engine-specific file first with a 404 falling back to the shared one), so nothing is read from the source tree. A client is opened per read, since the NUnit analyzer refuses a disposable field without a one-time tear-down and the case list is read at discovery time. The readers are async and awaited from the test (e8706de); the case list, which NUnit discovers synchronously, uses the client's synchronousSendrather than blocking on a task. Open for the author: whether the corpus pages should instead be written out explicitly in each binding's test (with a check againstindex.jsonso a new page cannot be skipped silently), a cross-binding choice the maintainer raised; theindex.jsonthe fixture reads over HTTP is the copy the web server project carries into the build output (its csproj linkscommon/src/web/**with copy-to-output, and its Bazel target lists//common/src/webas data), the same way the shared web extensions reach the test build.6ff78ca,baad8a3): the hand-written recording executor and driver subclass are replaced by Moq (Mock<ICommandExecutor>answering every command with success,Mock<WebDriver>calling the base), the tests assert only what is thrown and nothing about the commands sent, sinceICommandExecutoris slated for removal; neither fixture sets a parallel scope, which the project will turn on in one place; and the tests are namedMember_Scenario_ExpectedResultas the other unit tests are, one case per test (12 tests across the three fixtures, all passing locally under NUnit).Browser.Remoterun gets its WebSocket URL too. A change that madeAppServer.StartAsync()idempotent was reverted as out of scope (3554a3c); the double start it guarded against predates this PR and does not affect the snapshot tests.ToText()keeps the ADR's name all five bindings share; the text form joins lines with\non every platform because it is compared byte for byte with the corpus and the other bindings (now said in a comment); the JSON handling isJsonDocument/JsonElementonly, so it is AOT-friendly; the state keys stay strings, since they are the wire names the corpus and the publicStatesdictionary use.4ce3934sorts theSystem.Text.Jsonusing ahead of the project's, as the Format job'sdotnet format stylewants.🔧 Implementation Notes
core/accessibility/, which neededATOM_SRCSand the tsconfigincludeto becometypescript/core/**/*.ts.generic, so the ADR's pruning rule treats it like adiv.input type=passwordis atextboxandsummaryis abutton; frames report the ADR'siframetoken, and an explicit role on a frame never replaces it;titlebeatsplaceholderon text inputs; inline children concatenate without an added space (Save<b>now</b>isSavenow, as Chromium computes it); an embedded control's value is read ahead of its own label when it sits inside something being named;pressedand, for grids,selectedappear only when the element sets them (the ADR table says "whenever the role supports it" for both; this reading follows ARIA's stated default for the absent attribute); an ancestor'saria-disabledreaches focusable descendants only; a hiddenaria-ownsowner hides its owned elements. In the corpus, a label's text appears both as its control's name and as a text leaf, as Playwright's snapshot also renders it, and a treeitem's name from content includes its nested group, as accname's step 2F reads.getComputedStyle, not frombot.dom.isShown, and "focusable" for the presentational conflict rule is its own small check, notcore/focusable.ts. Two engine facts are encoded explicitly: Chromium decidesnoscriptrendering in layout rather than in its UA stylesheet, and closeddetailshides its content through a UA shadow tree script cannot see.focusedwhen focus is inside it, and its sheet's:host::beforeis read only while some sheet in scope defines generated content).SnapshotContextand dies with the call. A module-level cache would be faster on a second snapshot of an unchanged page and wrong on the first snapshot after any attribute change, because element identity does not change with the attribute.htmlelement as roledocument; an explicit root always produces a node; invalid roots throw.014-iframe) records the atom's view, withiframeleaves; each binding's test asserts the stitched tree for it separately. The corpus README's conventions may want a binding-level expectation for frame pages now that five bindings stitch them. Since the wire round, what a binding holds is only what is the binding's: the public node type and its text form, the mapping of references to its element class, and frame stitching across browsing contexts.Verification. The sandbox this was built in cannot fetch Bazel's external repositories, so the Bazel targets did not run there. On the current head
8fa4696every check is green (55 checks): Lint (buildifier, google-java-format, ruff, rubocop,dotnet format, prettier), CI (the per-language jobs and the Java and .NET browser jobs on Linux, macOS and Windows, the .NET remote job), RBE ("Test / All RBE tests", which runs the Python, JavaScript, Ruby, Java and .NET snapshot suites, including every new target below, on Chrome, Edge and Firefox) and CodeQL. The one failure the wire round met on CI was the Firefoxnullroot above, red on all five bindings' Firefox targets on81c9b20and green frome306305on.closure-test-safariis compatible with macOS only and no CI job runs the atoms suites on macOS, so Safari has not run. Locally the equivalent checks ran with the pinned tools: esbuild 0.27.0 with the repo's atom config,tsc5.8.3 with the package tsconfig (clean), the export generator's--checkmode, a Nodevmload of the bundle without a DOM, the two Node tests over the esbuild output, and every QUnit page in headless Chromium 141 against the shipped bundles, red before each implementation and green after, and unchanged through each performance commit. For the bindings, the generated BiDi layers were produced locally from the pinned CDDL with the repo's own generators, and the suites ran under pytest, mocha, rspec, the JUnit console launcher and NUnit as described above; CI runs//py:unit,//py:test-{chrome,firefox,edge}-bidi,//javascript/selenium-webdriver:small-tests, the per-browsertest-accessibility-snapshot-test-js-*targets,//rb/spec/unit:unit, theaccessibility_snapshot-*-bidiRuby targets,//java/test/org/openqa/selenium:SmallTests,//java/test/org/openqa/selenium/remote:small-tests, the per-browserAccessibilitySnapshotTesttargets in//java/test/org/openqa/selenium/bidi,//dotnet/test/unit/webdriver:unitand the per-browserAccessibilitySnapshotTeststargets in//dotnet/test/webdriver.🤖 AI assistance
core/accessibility/, the test bundle and renderer, the corpus pages and recorded expectations, the schema and Node tests, the benchmark page, the build wiring, the README sections, the review-fix and performance rounds, the Python, JavaScript, Ruby, Java and .NET bindings and their tests, the wire-shape round, the commit messages and this description, under the author's direction and following the ADR💡 Additional Considerations
iframeas the frame token and that a frame's explicit role never replaces it, thegenericmapping for no-role elements, the password/summary divergences, title before placeholder, treating a closed shadow host's light-DOM children as rendered, a hidden explicit root returning its node with no children, keeping the element structure (but not the text) below a node named from its content,pressed/gridselectedappearing only when set, first-owner-wins for conflictingaria-owns, a hiddenaria-ownsowner hiding its owned elements, inheritedaria-disabledreaching focusable descendants only, masking rather than omitting password values, the text form's rules (false states omitted, value after a colon with line breaks and backslashes escaped, description not rendered), whether the text form should also be computed in the atom, whether each binding's corpus test should list the pages explicitly rather than readindex.json(raised by the .NET maintainer; the same answer should apply to all five), in PythonWebDriverExceptionas the "unsupported operation" error for sessions without BiDi, since the binding has no dedicated class for it, in Ruby string keys forstates, in JavaUnsupportedCommandExceptionfor a classic-only session, adefaultinterface method throwingUnsupportedOperationExceptionfor other implementations, andAccessibilityNodeas a final class in the core package rather than an interface, and in .NET an async-only API (GetAccessibilitySnapshotAsync) on theWebDriver/WebElementclasses rather than the interfaces, with the driver opening one BiDi connection in its constructor for everyUseWebSocketUrlsession, shared by its internal BiDi components, so a session that asks for BiDi fails fast when the connection cannot be made (Java logs and continues; the .NET maintainer has asked that the cross-binding policy be settled by the TLC).NoSuchNodeExceptionin the Java BiDi layer and a typed error in the .NET one so the snapshotters can stop matching the error text; propagating top-level options to the driver-owned .NET connection's transport; measuring the BiDi wire cost of the bindings on the bench shapes; the out-of-CI comparison with@wdio/snapshotand native trees that the ADR §5 asks for, whose findings go into the corpus README's divergence list; a macOS job for the atoms suites if Safari coverage is wanted.javascript/atoms/andcommon/src/(test fixtures). The change is additive (new registry entries, new folder, two glob widenings, two fragments, one test bundle, two Node tests, a new corpus directory, one filegroup visibility line and onejs_library, one bench page, two.gitattributeslines; in Python one new module, two methods, onecopy_fileand one data entry; in JavaScript one new module, two methods, one export, one atom module target, one package entry, one lint data entry and one eslint ignore; in Ruby one new module, three methods, onecopy_file, two data entries and the signatures; in Java two new classes, twodefaultinterface methods, two overrides and one accessor, onecopy_fileand one resource, one filegroup visibility line and the test wiring; in .NET two new classes, two virtual methods, two internal accessors (the snapshotter and the driver-owned BiDi connection), one internal element helper, the constructor's BiDi connection, two disposal hooks and one resource entry, plus one small fix in the test driver factory); no existing atom's source or output and no existing fixture changes.🔄 Types of changes
🤖 Generated with Claude Code
https://claude.ai/code/session_016ubn295Cpb8fnrBgbXBMXw