Recursive AI

The Chrome accessibility-tree differential

All onehtml docs

OneHtml.Differential ax compares our accessibility tree with the one Chrome returns from Accessibility.getFullAXTree, node by node, for pages captured by tools/capture. Chrome is a black box here: only its protocol output is read. The command is in tools/OneHtml.Differential; nothing in src/ depends on it.

OneHtml.Differential ax <capture-dir> [--corpus <dir>] [--out <dir>] [--jobs n] [--limit n] [--timeout s] [--top n] [--facts on|off|both] [--dump]
ArgumentMeaning
<capture-dir>The capture tool's output directory (or its captures/ directory). Every …/<sha256>/<WxH>/ directory with a meta.json (or an errors.json) is one capture.
--corpusWhere stylesheets come from: a corpus version directory (manifest.jsonl, stylesheet bodies by URL) or a directory of loose .css files, matched by the end of the URL path. Chrome was served the same bytes. Without it, linked and imported sheets are missing and the cascade runs on what the page itself holds.
--outOutput directory, default ./out.
--jobs, --limit, --timeout, --topAs for the other commands: parallel captures (default 2), the first n captures in sha256 order, seconds per capture (default 30), classes shown in summary.md (default 20).
--factson, off or both (default). See "Rendered facts".
--dumpAlso writes dump/<sha12>-<viewport>.txt: one line per Chrome node with its counterpart, then our unpaired nodes. For finding out why a class exists. A Chrome node with no counterpart also shows where it sits (under its nearest paired ancestor), and one of ours shows its parent's path.

Exit status is 0 whatever the rates are; the numbers are the result.

What is compared

For each capture the tool reads dom.json, snapshot.json, ax.json and meta.json:

  1. Chrome's serialized DOM (doctype plus outerHTML) is parsed with OneHtml. The cascade gets the capture's viewport (width, height, deviceScaleFactor) as media features, the page URL as base URL, and the stylesheet resolver above. Scripts never ran; that is how the capture was taken.
  2. The snapshot is mapped onto the document (DomSnapshotAdapter.Map). A snapshot of a different DOM is refused; the capture is counted as refused with the refusal reason and not compared, because nodes cannot be matched without the mapping.
  3. Our tree is built (AccessibilityTree.Build, with AccessibilityOptions.BaseUrl the capture's final URL) and written in the Chrome-shaped export (onehtml-ax/1). Chrome's main-frame nodes and the export are read by the same code into a common shape, so both sides pass through the same normalization.
  4. Nodes are matched, then compared.

Only the main frame's nodes are compared; frames of other documents, and the child ids that point into them, are ignored.

Matching

  1. The root web area pairs with our document node.
  2. A Chrome node with a backendDOMNodeId pairs with our node for the same DOM node. The id is translated through the snapshot's backendNodeId and the mapping's node accounting (NodeFacts.BackendNodeId).
  3. Anonymous nodes. A Chrome node that has no OneHtml node (no backend id, or one the mapping does not carry: generated text, layout boxes, user-agent shadow content) pairs with the first still unpaired child of the counterpart of its nearest paired ancestor that is of the same kind (text or not), has the same role and the same ignored flag, and has the same name. Anything else stays unpaired. The number paired this way is in the summary.
  4. Chrome's InlineTextBox nodes are skipped on read. They are layout boxes of a StaticText and carry no information the static text does not.
  5. Unpaired nodes are counted per side. An unpaired node that is ignored is counted separately (chromeOnlyIgnored, oursOnlyIgnored): an ignored node is not part of what assistive technology walks, so it does not change the exposed tree. Chrome omits visibility: hidden and aria-hidden elements and noscript; we keep them as ignored nodes.

Fields

Each paired node is compared on the fields below. A field is compared when it applies; compared in the report is the number of times it was.

FieldComparedNotes
ignoredevery pairChrome's ignored against ours. If the flags differ, no other content field of that pair is compared.
hiddenevery pairWhether the ignored reasons include notRendered, ariaHiddenElement, ariaHiddenSubtree, inertElement or inertSubtree.
ignoredReasonsboth ignoredThe remaining reasons (uninteresting, presentationalRole, labelFor, …) as a sorted set.
roleboth not ignoredChrome's role value (including its internal roles) against ours (InternalRole or the Core-AAM name).
nameboth not ignoredAfter the normalization below.
nameSourcenames equal and not emptyThe step that produced the name, in these categories: aria-labelledby, aria-label, native-element (label, legend, caption, figcaption, SVG or document title element), native-attribute (alt, value, …), title (the attribute), placeholder, contents, text (static text). For Chrome it is the first entry of name.sources that is not superseded and has a non-empty value.
descriptionboth not ignoredAfter the normalization below.
valueeither has oneThe widget value.
prop.<name>either has itfocusable, focused (not the root: the page's own focus), disabled, checked, expanded, selected, pressed, required, invalid, level, live, modal, hasPopup, multiselectable, readonly, valuemin, valuemax, valuetext, orientation, multiline, editable, url (not the root: the document's URL is not in the DOM), posinset, setsize, autocomplete, busy, atomic, relevant, roledescription, keyshortcuts.
rel.<name>either has itactivedescendant, controls, describedby, details, errormessage, flowto, labelledby, owns: the related nodes in order, as OneHtml node indexes.
parentevery pairParent in the unpruned tree (ignored nodes included): the pair of Chrome's parent against ours.
exposedParentevery pairNearest ancestor that is not ignored.
childOrdernot-ignored pairs with at least two common exposed childrenThe order of the exposed children that both sides have. A child present on one side only is reported once, as chromeOnly or oursOnly.
chromeOnly, oursOnlyevery not-ignored node of that sidePresent on one side only.
chromeOnlyIgnored, oursOnlyIgnoredevery ignored node of that sideAs above, for ignored nodes.

Normalization

Everything that makes the two sides spelled differently without a difference in meaning is in AxNormalizer (tools/OneHtml.Differential/AxModel.cs), nowhere else. It is:

  • White space. Runs of ASCII white space (space, tab, line feed, form feed, carriage return) in names, descriptions and valuetext collapse to one space and the ends are trimmed. A no-break space stays.
  • Flags. false and absent are the same for focusable, focused, disabled, required, readonly, modal, multiselectable, multiline and busy; invalid=false is absent; an empty valuetext, roledescription, keyshortcuts, autocomplete or relevant is absent. Chrome reports these flags as false where the role supports them and we leave them out. expanded, selected, checked and pressed are not touched: absent means the role does not have the state, false means it is off.
  • Numbers. Property numbers are compared in their shortest round-trip spelling (1 and 1.0). The value of a meter, progressbar, slider, spinbutton or scrollbar is compared as a 32-bit float, because Chrome stores these as single-precision floats and reports them widened (0.6 arrives as 0.6000000238418579). Other values compare as written.
  • Value and flags added by the last round. An empty value is absent; atomic=false is absent; busy as 1 is true.
  • Parents. parent and exposedParent are compared through the nearest ancestor that has a counterpart on the other side, so an unpaired ignored node is not counted twice.
  • Role of ignored nodes. Both sides call an ignored node none; its role is not compared.
  • Name source of static text is text on both sides.
  • Names and descriptions of ignored nodes are not compared (neither side has them).

Nothing else is normalized: no role aliases, no case folding, no Unicode normalization.

Rendered facts

--facts selects whether our tree is built with the capture's rendered facts (AccessibilityOptions.Facts). The facts are always used for the mapping in step 2, in both modes.

  • off: one run without facts.
  • on: one run with facts; the report is that run.
  • both (default): each page is compared twice, with and without facts, over the same Chrome nodes. The per-field table and the classes are the run with facts; the section "With and without rendered facts" prints both rates side by side (with the number of mismatching nodes in brackets).

A capture whose snapshot does not map onto its DOM is refused in every mode.

Output

Written to --out; nothing in it should be copied into the repository except reduced, hand-written cases.

FileContent
summary.mdCounts, refused and failed captures with reasons, the per-field table, the with/without facts table, the top classes with up to three examples each.
summary.jsonSchema onehtml-ax-differential/1: reference (Chrome product, revision and protocol version from the capture's environment.json), facts, captures, compared, pages, refused, failed, chromeNodes, oursNodes, matched, matchedAnonymous, refusals (sha256, viewport, url, reason), fields (field, compared, occurrences, nodes, pages, pagesCompared, rate), topClasses.
classes.jsonSchema onehtml-ax-differential/1/classes: every class (field, pattern, occurrences, nodes, pages, examples). An example has page (sha256), viewport, url, node (our DOM path such as html[1] > body[2] > div[6], or chrome#<backend id> <role> for a Chrome-only node), chrome and ours (values, clipped to 80 characters). Ordered by occurrences, then field and pattern.
captures.jsonlOne line per capture: sha256, viewport, url, status (ok, refused, skipped, error), detail, node counts and mismatch count.
dump/With --dump, see above.

Rates. occurrences counts mismatching (capture, node) pairs; nodes counts distinct (page, node) pairs, so a page captured at two viewports counts its nodes once; pages is the number of pages with at least one. rate is occurrences over compared. For properties and relations, compared counts the pairs where either side has the property, so a rare property that always disagrees reads 100%.

Classes. A class is a field plus a pattern: for role, the two roles (LayoutTable -> table); for name, description and value, the role and the shape of the difference (chrome-empty, ours-empty, ours-prefix, chrome-prefix, case-only, differs); for properties, the role and both values; for ignored, which side is ignored and the roles and reasons; for the structure fields, the roles of both parents; for the presence fields, the role and, when ignored, the reasons. Output is deterministic: captures are processed in sha256 and viewport order whatever --jobs is.

Worker run

The command takes the worker's shared-job lock like the other corpus commands and exits with status 3 at once if it is held:

bash tools/OneHtml.Differential/run-worker.sh ax ~/onehtml/capture-out/<version> ~/onehtml/out/ax --corpus ~/onehtml/corpus/<version> --facts both --jobs 8

--limit N takes the first N captures by hash and --pages <sha256 prefix,...> selects pages; --dump writes a per-node listing of the selected pages.

Results with and without rendered facts

Chrome for Testing 156.0.8078.47 (protocol 1.3), headless, 1280x800, page script off; .NET 10 on the AMD Ryzen 9 7950X3D worker (32 threads), --facts both --jobs 2 --limit 2100, about 125 seconds. The sample is the first 2,100 captures by hash: 2,095 compared (1,840,086 Chrome nodes, 1,973,233 of ours, 1,779,203 matched, 284,249 of them by the anonymous-node rule) and 5 refused (a custom element whose shadow root the snapshot omits, and two whose text differs in line breaks). Rates are mismatching nodes over the times the field was compared, with the number of nodes in brackets. "Before" is the same pages with the tree as it stood at the start of this round (facts on). Fields that never differ are left out; the run's summary.md has all of them.

fieldbefore (facts)without factswith facts
ignored0.291% (5114)13.279% (199263)0.173% (3086)
hidden0.06% (1053)0.088% (1323)0.06% (1072)
ignoredReasons0.124% (491)1.68% (1380)0.118% (475)
role0.089% (1216)0.485% (5914)0.089% (1217)
name0.591% (8027)0.561% (6845)0.498% (6845)
nameSource0.012% (102)0.014% (102)0.012% (102)
description0.135% (1836)0.148% (1810)0.132% (1810)
parent0.307% (5398)0.422% (6332)0.303% (5397)
exposedParent0.389% (6851)23.639% (354741)0.245% (4362)
chromeOnly1.979% (27510)11.866% (164933)0.913% (12687)
oursOnly0.347% (4733)8.231% (126706)0.378% (5222)
chromeOnlyIgnored11.737% (52827)38.774% (174511)10.708% (48196)
oursOnlyIgnored32.173% (189519)79.531% (341912)31.864% (188808)
prop.checked0.039% (2)0.039% (2)0.039% (2)
prop.disabled6.931% (21)6.623% (20)6.931% (21)
prop.editable0.028% (1)0.028% (1)0.877% (61)
prop.expanded0.097% (5)0.096% (5)0.097% (5)
prop.focusable0.02% (58)0.02% (58)0.02% (58)
prop.hasPopup0.635% (19)0.635% (19)0.635% (19)
prop.invalid32.836% (22)32.836% (22)32.836% (22)
prop.level0.011% (22)0.011% (21)0.011% (22)
prop.posinset100% (28)100% (28)100% (28)
prop.readonly17.949% (7)17.949% (7)17.949% (7)
prop.roledescription5.069% (11)5.069% (11)5.069% (11)
prop.selected0.377% (58)0.377% (58)0.377% (58)
prop.setsize100% (28)100% (28)100% (28)
prop.url1.061% (2832)1.061% (2832)1.061% (2832)
prop.valuemax2.572% (8)2.572% (8)2.572% (8)
prop.valuemin1.929% (6)1.929% (6)1.929% (6)
prop.valuetext9.565% (11)9.565% (11)9.565% (11)

Without facts the tree cannot know what Chrome did not draw, so ignored, exposedParent, chromeOnly and oursOnly are high; with facts they are the figures above. No field is worse with facts than without by more than a few nodes, except prop.editable (1 node without facts, 61 with: the inner editors of text fields, see the classes). The probe pages (testdata/rendered/pages/*probe*.html, with Chrome's captures beside them) pin the rules that came out of this round: ChromeProbeTests compares the exposed structure of the generated-content probe, the inner nodes of text fields and the name of each of 170 links with Chrome's.

Classes

A class is a field plus a pattern (see "Output"). Dispositions of every class above 0.1% of its field's compared count (for the small property fields, above one node). Fixed means the class shrank to the figures given, with a hand-written test and a profile entry; open means it was not traced to a rule and is not documented as a deliberate difference.

ClassCountDisposition
oursOnlyIgnored (none with notVisible, notRendered, ariaHiddenSubtree, ariaHiddenElement, uninteresting, emptyAlt, presentationalRole)about 189,000Representation, deliberate. Our tree keeps every DOM node with its hidden and ignored reasons so that edits and diffs map back to the markup. Chrome omits nodes it has no use for. An ignored node is never part of the exposed tree. Not fixed.
chromeOnlyIgnoredabout 48,000Representation. Chrome creates ignored nodes (children of display: none and aria-hidden subtrees, uninteresting wrappers around layout objects) that have no DOM node of ours. Ignored, so not exposed. The wrappers of ::before and ::after boxes are now ours too (profile entry generated-content-node).
ignored: Chrome generic, ours ignored uninteresting2,026 -> 1,014Fixed in part, open. The residue was traced: a generic with drawn ::before or ::after text is kept (fixed, 1,000 of them), an inline-block span is kept (fixed; the earlier rule ignored every span), and what is left are spans and divs of the same shape that Chrome ignores (inline-block spans with and without generated content split about 60 to 40, div and span flex items with a text child), where nothing in the capture's styles separates the two. Probe pages show Chrome ignoring a block-level empty ::before box in a block host and keeping it in an inline host.
ignored: Chrome ignored uninteresting, ours generic716 -> 290Open, small. Mostly an inline-block span whose Chrome node is ignored.
ignored: Chrome LabelText, ours ignored labelFor (and the text children)485 -> 395 on 15 pagesFixed in part. A probe of 50 label forms settled the rule (profile entry label-for): a label with only text that labels a checkbox or a radio button is labelFor whether it wraps the control or uses for, before or after it; a label with an element child, or of any other control, is LabelText. The 395 left are on 15 pages and were not traced.
ignored: Chrome image, ours ignored emptyAlt362Deviation. An img with alt="" and a title is an image in Chrome 156 (47 of 47 in 400 pages); HTML-AAM and WPT html-aam/roles-contextual.html (el-img-empty-alt-title) give it no role. Kept at the specification's answer.
hidden (1,072 nodes, 0.06%)1,072Below the threshold. The ariaHiddenSubtree against ariaHiddenElement split is Chrome's choice for nested aria-hidden that the profile entry ignored-reasons handles for the element's own attribute; the rest are notRendered subtrees of display: none ancestors that Chrome lists.
ignoredReasons (475 nodes, 0.118%)475Open, near the threshold. uninteresting against no reason on SVG graphics elements (Chrome gives notRendered to title, desc, stop and some path elements depending on layout objects we do not model) and uninteresting against presentationalRole in role=presentation tables.
parent, exposedParent5,400 and 6,851 -> 4,362Follows from the other classes. exposedParent fell from 0.39% to 0.245% with the generated-content and label fixes; the rest is the generic differences above and LayoutTableCell against LayoutTable.
chromeOnly: StaticText (4,082), generic (2,572)27,510 -> 12,687Fixed in part. ::before/::after text and boxes, the text of input buttons and the inner editors of text fields are now nodes (profile entries generated-content-node and control-text-node); the part that is left is the shadow content of date, time, file, range and select controls, the pseudo boxes of display table, flex, grid and list-item (Chrome nests them differently), image-input parts, a StaticText that Chrome gives to whitespace Chrome draws between inline elements that are not both display: inline, and the ListMarker and listitem of ::before boxes with display: list-item. Open.
oursOnly: StaticText (3,184), generic (1,688)4,733 -> 5,222Open. Whitespace text nodes Chrome merges with a neighbour, and generics of ours that Chrome has no node for at all: an a without href is now left out like a span (fixed), but inline-block spans and block a/div wrappers remain. The oursOnly rate rose from 0.35% to 0.38% while chromeOnly fell from 1.98% to 0.91%: more nodes are created, some of them not Chrome's.
name: link differs7,800 -> 4,392Fixed in part. Flex and grid items and display: contents elements are blocks for the spaces in a name (profile entry positioned-blockification, 150 of 150 probe links agree), SVG style text is not name content (176 links), the rest is title and aria-label of hidden children Chrome includes, alt of images Chrome leaves out (a link with a lazy-load placeholder image), the spaces Chrome puts in a long text that wraps inside a word (a URL as link text) and spaces of icon-font spans. Open.
name: term (929), time, figure, heading929 + 114 + 392 + 186dt named from its contents and time unnamed (profile entry name-from-contents-roles, fixed). A figure whose name we take from a title or figcaption that Chrome leaves empty (392 on 78 pages) and a heading inside a button whose name we leave empty (186 on 34 pages) are open.
name: option case-only147 on 7 pagesDeviation, open. Chrome does not apply the select's text-transform to an option's name; we do.
role (1,217 nodes, 0.089%)1,217Below the threshold. cell, row, table against LayoutTableCell, LayoutTableRow, LayoutTable (profile entry layout-table), and image against SvgRoot.
description, nameSource1,810 and 102Open, small. description is 0.132%; the remainder is the name classes (a link's title as description when Chrome uses it as the name).
prop.url: image and srcset2,832Deviation. Chrome reports the image chosen from srcset and picture for its viewport and density; we state the src, because choosing needs the layout width.
prop.editablesee belowSecond pass ("Property fields, second pass"): traced and fixed, or documented per class.
prop.disabled, invalid, hasPopup, readonly, selected, posinset, setsize, valuemin, valuemax, valuetext, roledescriptionsee belowSecond pass ("Property fields, second pass"): traced and fixed, or documented per class.
valuesee belowSecond pass ("Property fields, second pass"): traced and fixed, or documented per class.
rel.labelledby, rel.controls298 and 14Open. Chrome lists the label for a radio, checkbox and textbox where our name came from another source; navigation landmarks whose label target is not in Chrome's tree.

The open classes above are not traced to a rule. The largest are name link differs (0.3% of the field's compared nodes), the generic and StaticText differences in chromeOnly and oursOnly, and the three small ignored residues.

Property fields, second pass

The first run of the full corpus (12,788 captures, facts both) left these property fields above 1%: posinset and setsize 100%, invalid 27.3%, readonly 21.5%, disabled 15.8%, valuetext 7.9%, value 4.9%, roledescription 2.3%, selected 1.7%, valuemax 1.2%, autocomplete 2.8%, hasPopup 1.0%, editable 1.1%, keyshortcuts 1.0%, and chromeOnly 1.126%. Each had one or two systematic causes. They were found by writing a probe page for each suspected behaviour (testdata/rendered/pages/axprops.html, axprops2.html, axprops3.html), capturing it in the pinned Chrome (captures beside it in testdata/rendered/captures), and fixing the tree or export to match; the rule is a profile entry of the name given. ChromePropertyTests has one case per cause; ChromePropertyProbeTests compares every element with an id on the first two probe pages property by property and lists the three differences left; ChromeProbeTests compares the inner-node structure of the third.

Causes

FieldCauseProfile entry
posinset, setsizeChrome never states either (214 of 214 mismatches had Chrome absent), whether the author gave them or not; we stated the given ones.export-set-position
invalidChrome reports constraint failures of more control kinds than we did (radio groups, required file, date and time fields, list boxes, email and url syntax, pattern, step, date range) and none for a disabled or readonly control or after an explicit aria-invalid=false.constraint-invalid
readonlyChrome states it on text boxes and grids only, and not when disabled.readonly-exposure
disabledA disabled ancestor reaches only the focusable descendants, a disabled fieldset or optgroup is not itself disabled, an option of a disabled select is.disabled-inheritance
valuetext, valuemin, valuemax, value of rangesChrome never states aria-valuetext, reports bounds as single-precision numbers, lets the author's aria-valuemin/max/now win over native attributes, and gives a spin button 0, 0 and 0 and a progress bar with a bad aria-valuenow the value 0.range-values
value of text-like controls"No file chosen", password bullets, the text of a custom combobox or text box, the first enabled option of a select (not the first), lower-cased colours, no value for an invalid date, no line breaks in a text value, the select's text-transform.input-password-file, option-selectedness, text-values
selectedChrome omits selected=false on an option, tab or tree item when another item of the list says anything about aria-selected; it states none on a disabled option; a tree item is false by default.selected-states
role (a by-product)An option role with no listbox around it (a button, a or li carrying role=option in a popup) is the element's own role in Chrome. This removed about 7 in 8 of the role mismatches on the 171-page subset.option-needs-listbox
roledescriptionNot stated on a generic element; non-ASCII text arrives as Latin-1 bytes of its UTF-8 form.roledescription-roles
autocomplete, hasPopuphasPopup on any element, listbox by default for every combobox, aria-haspopup=1 and false are not popups; autocomplete on text fields only, and for input list.autocomplete-popup, autocomplete-text-fields
keyshortcutsThe accesskey attribute as written, after Alt+.accesskey-shortcut
editableEditable text inherits down from a contenteditable host; the inner editor of a disabled or read-only field is not editable.editable-inherited
expanded, level, multiline, required (found by the probe comparison)An ARIA combobox has no expanded of its own, tree items have levels, a contenteditable text box is multiline, a list box select is required.combobox-expanded, treeitem-level, readonly-exposure, text-values
chromeOnly generic (a search field with a placeholder)The placeholder leaf comes before the ignored node that holds the inner editor of a search or number field, and a combobox input has that node too.placeholder-leaf

Before and after

Same machine and run as above (Chrome for Testing 156.0.8078.47, --facts both --jobs 2, one process; AMD Ryzen 9 7950X3D, 32 threads, load average 4 to 5 from other work during the runs, so these are counts, not timings). Counts are mismatching nodes with facts. The subset is the 171 captures whose first mismatch examples were in the classes above (every class had at least one page); the sample is 399 captures from the middle of the corpus order (--skip 3000 --limit 400, one refused), as a check that no other field gets worse.

fieldsubset beforesubset aftersample beforesample after
ignored127127730730
hidden738738219219
ignoredReasons273273222222
role57182228228
name5865863,9323,932
nameSource23235959
description452452498498
parent627627609609
exposedParent235235988988
chromeOnly8747992,3802,267
oursOnly737693925820
prop.posinset, prop.setsize151 each000
prop.invalid45010
prop.readonly36010
prop.disabled791310
prop.valuetext83030
prop.valuemin, prop.valuemax9, 210, 000
prop.roledescription190110
prop.selected571181810
prop.autocomplete6000
prop.hasPopup61060
prop.editable73430
prop.expanded10020
prop.level29144
prop.keyshortcuts9000
prop.required, prop.multiline2, 10, 000
prop.url, prop.focusable, rel.*unchangedunchangedunchangedunchanged
value864022

The final code was run on both sets. Every field not named is equal; none rose. The final rates come from the next full-corpus run.

What is left, per field

FieldLeftWhy
disabled13 on the subsetAn a inside an object (the plug-in fallback content) is disabled in Chrome, with no ancestor that says so. Not traced further.
selected18 on the subsetA tab in a tab list of aria-multiselectable accordion panels states nothing in Chrome although no tab says anything; we state false. A row with aria-selected=false. The rule behind the first is not clear from the probes.
value40 on the subsetA button with the combobox role whose content has an svg with a title: Chrome's value includes the title ("1 downwardArrowQuantity"), ours the text only (30). A select with a trailing direction mark (\u200e) that Chrome trims (2). A custom combobox whose hidden part Chrome leaves out of the value (1). A text field with contenteditable=false and role=textbox, whose text Chrome joins with its line breaks (3). Other select cases (3). An input with role=status (1).
autocomplete, hasPopup, keyshortcuts, invalid, readonly, roledescription, valuetext, valuemin, valuemax, posinset, setsize0 on the subsetFixed.
editable4 on the subsetStatic text inside an editing host that our tree does not mark; not traced.
prop.urlabout 0.9%srcset choice, see above; not in this unit's list.
chromeOnly830 on the subset (0.6%)Not reduced to one cause. The largest single class found by tracing is a harness artefact: pages that link their stylesheets with crossorigin from another host (a CDN, with a subresource-integrity hash) load in our cascade but are rejected by Chrome in the capture, because the stored response has no CORS headers. A Bootstrap .modal { display: none } hides the dialog for us and not for Chrome, so Chrome's whole dialog subtree is chromeOnly (traced on one page; whether the same cause is behind the 80-page listitem and list classes was not checked). Fixing it means serving the stored sheets with Access-Control-Allow-Origin in the capture tool, which needs a re-capture; it is not a library difference. The rest is the generic, StaticText and ListMarker classes above.

Behaviour not copied

  • The Chrome value of an editing host with block content (x\n\nbs for a div holding a p, a button and a span) depends on layout line breaks; ours has none (ChromePropertyProbeTests).
  • aria-selected=false on a row: Chrome states nothing, we state false (as for the grid cell and option, which state it).
  • Step mismatch counts only from a min (the step base is the min, else the value itself).

Findings on the fixture pages

The fixture pages (testdata/rendered/pages) were captured with Chrome 156 (headless, script off, 1280×800). After the fixes listed in the profile, what is left is below, each with a verdict. "Chrome" means Chrome's choice where the specifications leave room or are silent; "ours" means we do not yet do what Chrome does and no specification says otherwise. Evidence is Chrome's output on the page named.

ClassVerdict
chromeOnly: ListMarker, generated StaticText (::before/::after content), LineBreak, MenuListPopup, the shadow content of date, number, textarea and select controls, the StaticText of a submit button's valueNeeds rendering; partly done. Chrome creates these from layout objects and user-agent shadow trees. They have no DOM node in the document. With facts the list marker is a node of ours (profile entry list-marker-node) and the select popup too (layout-select-popup); generated ::before and ::after boxes and text (generated-content-node), the text of input buttons and the inner editor of text fields (control-text-node) are nodes too; the shadow content of date, file, range and select controls is not. HTML-AAM and CSS Content give generated content a place in names (done in the name computation) but not in the tree.
role: LayoutTable, LayoutTableRow, LayoutTableCell against table, row, cell, genericChrome. Chrome gives the internal layout-table roles to tables it judges to be for layout (a one-cell table, display: table boxes) and keeps table, row and cell for data tables. HTML-AAM maps table to table. Without facts the heuristic is not reproduced. With facts it is (profile entry layout-table), fitted on 171 tables of the corpus sample.
ignored/ignoredReasons: a label whose control is a checkbox or radio button is ignored with labelFor, and its text is presentationalRoleChrome, observed on three labels, all of them checkbox or radio labels with the control first. label elements around text fields (control after the text) are LabelText. Which of "checkbox or radio" and "control first" decides is not determined by the fixtures. A capture of <label>text <input type=checkbox></label> and <label><input type=text> text</label> would settle it.
ignored, ignoredReasons, role: cells and rows of a table with role=presentationChrome differs from the inherited-presentational rule we follow (WAI-ARIA 1.2 section 5.2.7): its tbody and tr are ignored as uninteresting and its td are generic and exposed. We make them all presentationalRole. Observed on one table.
ignored: templateChrome, unexplained: a template directly in the body is generic, a template in a ul is ignored. We ignore both (a template has no rendering).
prop.focusable on iframeChrome does not report an iframe as focusable. HTML section 6.6.3 lists navigable containers as focusable areas, so we do.
value: No file chosen on a file inputReproduced since the second pass (profile entry input-password-file).
name: mi text 𝑥 against xChrome applies text-transform: math-auto from the MathML Core user-agent style to a single-character mi; our cascade does not implement that value.
parent: children of b, i and visibility: hidden elementsRepresentation. We keep these as ignored nodes (the unpruned parent therefore names them); Chrome has no node. exposedParent agrees.
parent, exposedParent: option and optgroup under MenuListPopupChrome puts the options of a drop-down select in a popup node that exists only while laid out; ours are children of the combobox.
oursOnlyIgnored: noscript, visibility: hidden, aria-hiddenRepresentation, see "Matching".

Profile entries added or confirmed from this run are in accessibility-chromium-profile.md (evidence "observed in Chrome output"). Unobserved and worth a capture: an empty div and span; u, s, small and other inline formatting elements; a nested list; <label> with each control type and position; the other date and time input types; nested tables and role=presentation tables with th.