Skip to content

fix(pixel): capsule role/name locators match Playwright's source-text names as well as Chromium's text-transformed names - #6750

Merged
Lightheartdevs merged 6 commits into
mainfrom
claude/capsule-role-name-source-text
Sep 26, 2026
Merged

Lightheartdevs merged 6 commits into
mainfrom
claude/capsule-role-name-source-text

Conversation

@Lightheartdevs

@Lightheartdevs Lightheartdevs commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

What this is, and what it is not

This fixes one false negative in the preview inspector. An exact role/name locator for assert-visible or click returned no_match when the button was styled with CSS text-transform, even though the owner's exact name is in the source and Playwright's own getByRole finds the button.

It is not the round-107 regression fix, and it should not be counted as one. The two adversarial reviews of this PR replayed the recorded sessions: real capsules of main and this branch in real Chromium, pages rebuilt from each session's write/edit calls and digest-verified, and receipts fed through the real inspect tool and tool-loop-guard hooks. I re-read their receipt and guard outputs. They found:

  • Recorded r106/r107 small-model website failures rescued: none of four.
    • laptop r107: 1 of 9 capsule receipts changes (the recorded fix(windows): use GGUF filename as OpenCode model ID #51 plan, tool call 24). no_match becomes step 3 click passed, then step 4 .sold-out-card visibility_mismatch. That is still a failure: the page's CSS hides the card and its script removes a class the card never has. The guard's failure count is identical and the no-progress stop lands on the same call (feat: standalone Windows installer (NVIDIA + AMD Strix Halo) #40, the 12th total failure) on both capsules.
    • strixy r107: the capsule was never contacted. The first inspection was rejected for invalid arguments (flat selector/role keys), and the next two calls were tool_call with id tool_describe.
    • mac-mini r106 and r107: CSS selectors only, so the receipts are identical.
  • The only small-host role/name miss this turns from fail to pass in the 58 surveyed rounds is laptop r101. That model had already recovered on its next call with #revealBtn, and the journey passed.
  • Round-105 passing sessions (laptop e63bc36d, strixy 6ebf74a3, including fix(pixel): report an untested requested show/hide change as incomplete at tool-result time #6740's ready plan with button "Show sold out"): every receipt is identical, and so is the guard's failure and stop sequence.

The laptop r107 case now ends on a measured visibility_mismatch, and on main that gets the generic line "Requested behavior remains unverified; a failed inspection does not establish a visibility transition." Open PR #6748 replaces that line with a measured diagnosis of the step (locator, display, visibility). That message change belongs there, not here.

What was wrong

The capsule matched exact role/name locators for assert-visible and click only through Chromium's Accessibility.queryAXTree (preview_inspection_capsule.py, measure()). Chromium's accessibility names apply CSS text-transform.

So a button whose source text is exactly Show sold out, styled text-transform: uppercase, is named SHOW SOLD OUT there. The owner's exact name returns no_match. The feedback then tells the model to copy the exact name from its source, which it already did.

The capsule's own click (frame.get_by_role(role, name=..., exact=True)) and the owner harness (preview-check.mjs, getByRole exact) both use Playwright's names, which are the source text. Both see the button.

The same gap shows up with whitespace. Chromium keeps the space after an aria-hidden icon, so the button is named " Show sold out", while getByRole normalizes it. #6741 recorded that case on its round-100 icon variant as a known no_match, "so a change is noticed". On this branch that click passes (see the last commit).

Checked firsthand:

  • Local HeadlessChrome/149.0.7827.55: queryAXTree(accessibleName: "Show sold out", role: "button") returns [] for an uppercase-styled button. "SHOW SOLD OUT" matches, and the full AX tree lists SHOW SOLD OUT.
  • Playwright 1.62.0 (the pinned engine), on the parity fixture in this PR: getByRole('button', {name: 'Show sold out', exact: true}) finds that button. Chromium's AX query does not.

Fleet evidence (round directories under ods-main-qualification-20260924-r049, read-only). These are all the role/name no_match steps in rounds 100–107:

Round Host Session Step Site
r101 laptop 119e5fd5 #10 click button "Show sold out" site-458e7785
r107 laptop fabc87a8 #52 click button "Show sold out" site-1f5f2cf8
r100 tower2 1b442bc3 #20, #28 click button "Show sold out" site-26ed2636, site-457c4e51
r102 tower2 22641527 #40 assert-visible button "Show sold out" site-fb63ab5e
r103 tower2 c719f29c #34 click button "Show sold out" site-b438e588
r107 tower2 a674292a #46 click button "Show sold out" site-9cae6c6f
  • laptop r107 (main d4a61f3): step 2 .show-sold-out-btn assert-visible passed. Step 3, click button "Show sold out", returned no_match. With this branch, that step runs and step 4 reports the page's real defect instead. As above, this does not change how that run ended.
  • laptop r101: the same no_match. The model recovered only by switching to #revealBtn, and the same page then passed.
  • tower2 r100: site-457c4e51 is the script aria-label override that fix(pixel): verify owner-requested exact control names after the page scripts run #6741 addresses. Neither r100 source has text-transform. The other tower2 sources, both laptop sources and the r102 assert-visible case all style the button text-transform: uppercase, and the exact name is in the source every time (from the fix survey).
  • The harness's own getByRole-exact check recorded soldOutRevealed: true on these uppercase buttons in r101 and r105.
  • Other role/name no_match steps are real name mismatches and stay no_match. In tower2 r105/r107, heading "Sum of squares calculation" is written as "Sum of Squares Calculation" in the source, "Expense report cli" as "Expense Report CLI", and "Midnight sold-out concert" as "Midnight Sold-Out Concert".

The same false negative would also defeat ready plans built from the owner's quoted control name (#6740's transition_untested fallback) on an uppercase-styled control. No recorded session shows that case yet: in strixy r105 the ready plan's button had no text-transform.

What changed

Only host/preview_inspection_capsule.py changes behavior.

Shared rules, two exact-name matchers. #6741, now on main, moved the role and name rules into ACCESSIBLE_NAME_RULES, shared with its load-time CONTROL_NAMES. That includes a rendered (includeHidden: false) name mode and a port of Playwright 1.62's isElementHiddenForAria. This branch had ported the same rule; the merge keeps main's copy. This branch appends one exact-name walk, ROLE_NAME_MATCH, to those rules twice:

  • ROLE_NAME_INCLUDING_HIDDEN (renderedOnly = false): unchanged includeHidden: true behavior, still used only by assert-hidden.
  • ROLE_NAME_RENDERED (renderedOnly = true): Playwright's default getByRole(role, {name, exact: true}). It is used for assert-visible and click, including the after-click observation.

The two differ only in const renderedOnly, and a test pins that.

Rendered-only matching skips every element hidden for ARIA:

  • script, style, template and noscript
  • display: none or aria-hidden="true" on the element or on an ancestor across shadow hosts
  • an unslotted shadow-host child
  • an element whose own visibility is not visible, or whose checkVisibility() fails (content-visibility, closed <details>)
  • a display: contents element with no rendered child

Opacity does not matter, as in the AX tree.

The rendered name also leaves hidden descendants out, as Playwright's includeHidden: false name does. The exception: a hidden descendant still counts when it is reached through an aria-labelledby, <label> or SVG <title> reference that is itself hidden.

measure(): a role/name locator now always goes through the existing including_hidden(...) union, with rendered_only=not include_hidden:

  • Chromium's rendered AX matches are resolved and passed in, as before.
  • The matcher adds its own matches, de-duplicated by element identity.
  • The MAX_RENDERED_MATCHES bound, the unresolved-node handling and the count/uniqueness rules are the same.
  • The unique union member is the node observed.

Linear cost (third commit). The matcher now runs on every role/name assert-visible and click sample, inside the 1.5 s observe window, so two superlinear paths in the shared rules were removed. They also speed up the existing assert-hidden matcher and CONTROL_NAMES; names and matches are unchanged.

  • hiddenForAria results are kept per element per call, like Playwright's cacheIsHidden. Before, a nested display: contents chain was re-walked from each of its levels, so its cost was quadratic in its depth.
  • Labels are indexed once per tree per call by label.control, which gives the same list, in tree order, that element.labels returns. Before, element.labels made Chromium scan the whole tree on each element's first read, so naming N buttons cost N x elements.

Unchanged: the assert-hidden semantics, the click dispatch, receipt schema, plan hash, capsule deadline, and every model-facing string. The protocol doc's locator-matching section is updated.

Safety invariants and behavior changes

  • Names stay exact. No case folding and no normalization beyond the matcher's existing whitespace and zero-width handling. The tests cover show sold out, Show Sold Out and Show sold out! returning no_match.

  • Superset of Chromium. Chromium's own matches are still passed in and kept, so every element Chromium matched still matches. The tests cover SHOW SOLD OUT, Show All Events and River Lantern Walk still passing, and an AX-only display: contents + aria-label case.

  • Uniqueness counts the union, so a plan that passed on main can now fail with selector_not_unique. This happens when Playwright's rules find an element with the exact name that Chromium named differently, next to another match:

    • <button class=cap>save</button> (capitalize; Chromium names it "Save") next to <button class=up>Save</button> (uppercase; Playwright names it "Save"). Main passed both assert-visible and click on "Save" with count 1. It measured the first button, while Playwright's click hit the second. This branch reports count 2 for both.
    • Two buttons with the same source text, one uppercase-styled. Main passed assert-visible with count 1, and its click failed as click_failed because Playwright's strict click saw two elements. This branch reports count 2 for both.

    In each case main was measuring one element while the name addressed another, and the owner's getByRole exact check sees the same ambiguity. None of the 40+ recorded fleet pages the reviewers rebuilt and replayed hits it.

  • Still rendered-only for assert-visible and click. test_role_locator_semantics_outside_assert_hidden_are_unchanged is unchanged and still passes (count 0 for a heading in <article hidden> and for <button hidden>). New tests cover display: none ancestors, aria-hidden="true", visibility: hidden and content-visibility: hidden.

  • Isolated world, read-only. No DOM mutation and no page-world evaluation. test_matcher_source_is_bounded_and_read_only now checks both matchers. A new spoofing test overrides checkVisibility, getComputedStyle, getAttribute, hasAttribute, querySelectorAll and textContent in the page world, and matching is unaffected.

  • Bounds unchanged. MAX_RENDERED_MATCHES, receipt schema, planSha256, the 45 s capsule deadline, and the loop and progress bounds (RUN_PROGRESS_LIMITS untouched). No plugin change, and no prompt or tool-description byte changes.

  • A failing page still fails. The laptop r107 replay now runs the click and still reports the page's real defect as visibility_mismatch. Nothing here can turn an unverified site into a verified one.

Cost

Measured locally (HeadlessChrome 149, no CPU limit), in one session, on this branch before and after the third commit. "First" is the first matcher call on a fresh page, which is what the first stability sample pays.

Page Elements 04b78d7d 42493077
3000 event cards, uppercase buttons 18,006 820 ms first, 55 ms warm 56 ms first, 42 ms warm
1 button, 400-deep display: contents chain 405 63 ms warm 8 ms warm
3000 buttons, each a 20-deep display: contents chain 63,004 2,893 ms first, 1,489 ms warm 563 ms first, 481 ms warm
laptop r107 fixture 54 11 ms first 7 ms first

The existing assert-hidden matcher gains the same way: 796 ms to 49 ms first call on the 3000-card page.

End to end through run_browser, a role/name assert-visible plan:

  • 3000 cards (18k elements) and 5000 cards (30k elements): unstable before, passing after.
  • 8000 cards (48k elements): unstable before; after, it passed in 1 of 3 runs.

The merged head (2dc6b0b4, on top of #6741) measures the same: 3000 cards 76 ms first call, the 63k-element chain page 670 ms first call, and 3000 and 5000 cards pass end to end.

Remaining limit, disclosed. The cost is now linear, but it adds to Chromium's own AX query in every sample, and that query alone takes 150–450 ms on its first call at these sizes. On the reviewers' synthetic page (3000 buttons, each wrapping a 20-deep display: contents chain, 63k elements), a 12-step role/name plan was still unstable at step 1 in 4 of 4 local runs; a 3-step plan passed in 1 of 1. Main passed that page, because it ran the AX query only.

  • This fails closed: the step reports unstable, never a false pass.
  • Main's own assert-hidden role/name path is unstable on the same page (first sample 3.1 s).
  • CSS selectors are unaffected.
  • Fleet pages are 35–60 elements.

Deployment

The inspection image picks up the new capsule through the existing source copy:

  • installers/lib/pixel-preview-inspection.py includes preview_inspection_capsule.py in BUILD_FILES.
  • Dockerfile.inspection COPYs it.
  • The macOS native path uses the same build_config.
  • pixel-host-install.sh also includes it in the managed file list used for uninstall.

Every Pixel host install runs build from the current sources and binds the resulting image ID in /etc/ods-pixel-inspection.json. Docker's cache is keyed on file content, so changed capsule bytes produce a new image.

Checked read-only on tower2: the config written at the round-107 install (19:21 -04:00) binds sha256:ec4868a9…, labelled Playwright 1.62.0. The image itself was built at 10:05 the same day, which is consistent with a cache hit for unchanged capsule bytes.

Fleet note: a round that installs this candidate picks the change up. A journeys-only rerun without reinstall, like r101's "no reinstall" note, keeps the old image.

Merge order

#6741 landed first (main 6cba572f). This branch merged main and resolved the conflict onto #6741's shared rules, keeping one set of helpers (merge commit 2dc6b0b4):

  • main's ACCESSIBLE_NAME_RULES, hidden-for-ARIA helpers, reference kinds and SVG <title> names
  • this branch's hiddenForAria cache and label index, which now also serve CONTROL_NAMES
  • ROLE_NAME_MATCH appended with renderedOnly false or true

Git's auto-merge had silently kept main's single matcher definition carrying this branch's walk, followed by this branch's two definitions of a ROLE_NAME_RULES that no longer existed. There was no conflict marker for that part; it was fixed by hand.

Guarding the next composed tree:

  • The fourth commit adds tests/preview_capsule_scripts.test.mjs to the pixel-agent JS suite, which CI already runs in test-linux and portal-runtime.
    • It compiles every capsule isolated-world script, now including CONTROL_NAMES.
    • It runs both role/name matchers and CONTROL_NAMES on an empty document.
    • A duplicated const parentOf fails it with "Identifier 'parentOf' has already been declared".
  • CI still does not run ods/tests/test_preview_inspection.py. The ODS_PREVIEW_BROWSER_TESTS=1 suite was run on the composed tree here, and it caught one expectation that the merge made stale: fix(pixel): verify owner-requested exact control names after the page scripts run #6741's round-100 icon-variant click, which now passes. The last commit updates that test (details under Tests). The next PR that touches the capsule should run this suite on its composed tree too.

Follow-up for the plugin side (not changed here): #6741's load-time "that name is on the page" feedback (presentName in workspace-preview-inspect.mjs) tells the model that role/name matching "compares the browser's own name verbatim, which can keep extra spacing (for example beside an aria-hidden icon)". With this branch the icon page matches, so that feedback can now appear only for a control that changed between load and the step, and its explanation no longer applies. The JS capsule models in control_names_transition.test.mjs and requested_control_names.test.mjs still model the older capsule for the icon variant; they test the plugin's handling of that receipt and still pass. #6748 is rewriting that message, so the wording is left to it. COMPLETION-RELIABILITY.md is updated to say where the feedback still applies.

Other open PRs (checked with git merge-tree against this head):

Tests

Unit, fake CDP (SourceTextRoleNameTests):

  • AX returns [] for the source name and the rendered matcher finds the button: count 1, that node is observed, and the click is dispatched.
  • AX and the matcher on the same node: count 1.
  • Chromium's transformed-name match is kept.
  • A matcher hit that is hidden for ARIA gets no_match for assert-visible and click, and the hidden-inclusive matcher is never called. assert-hidden still matches it.
  • A Chromium match plus a distinct source-text match: selector_not_unique.
  • The recorded fixtures hash to the recorded siteId and sha256.
  • HiddenRoleLocatorTests is updated to record which matcher each step used.

CI-run compile guard (pixel-agent/tests/preview_capsule_scripts.test.mjs): every module-level capsule function script compiles. Both role/name matchers and CONTROL_NAMES run on an empty document, and the matchers keep the passed Chromium matches, de-duplicated by identity. test_names_share_the_matcher_rules_and_are_read_only (from #6741) now also checks ROLE_NAME_RENDERED.

Opt-in real Chromium (ODS_PREVIEW_BROWSER_TESTS=1, Playwright 1.62.0):

  • uppercase and capitalize buttons clicked, and a capitalize heading asserted visible, by their source names
  • transformed names still matching
  • exact-name negatives
  • hidden-for-ARIA exclusions and the opacity case
  • hidden descendants left out of the name
  • uniqueness
  • page-world spoofing
  • test_rendered_role_matcher_agrees_with_playwright_get_by_role: 47 role/name queries on one page, compared with Playwright's own engine. It covers text-transform, hidden and aria-hidden ancestors, visibility, opacity, hidden descendants, hidden aria-labelledby and <label> references, display: contents, content-visibility, closed <details>, ::before content, duplicates, shadow DOM (slotted, unslotted, shadow children), whitespace, and labels: several for one control, wrapping, a button's label, a label for a non-labelable element, and a shadow-tree label. The label cases are also added to the includeHidden parity test, which now has 40 queries with fix(pixel): verify owner-requested exact control names after the page scripts run #6741's SVG <title> and hidden-icon cases.
  • fix(pixel): verify owner-requested exact control names after the page scripts run #6741's test_load_time_control_names_replay_fleet_round100: the exact-name click on every repaired variant, including the aria-hidden icon, now passes with count 1. It was an expected no_match for the icon variant.
  • test_role_name_matchers_do_linear_work: counts style and label reads in the matcher's own isolated world, on a page with a 200-deep display: contents chain, 300 buttons and 3 labels. Both matchers stay within 8 style reads per element and 3 label reads per call.

Replays of recorded bytes (new fixtures; .gitattributes keeps them byte-exact):

Results:

All runs are on tower2 (Linux) from git archive of this branch into a scratch directory, using the existing Playwright 1.62.0 test venv and chromium_headless_shell-1234. Nothing was installed. No Pixel services or fleet hosts were touched.

Final head ada56f51 (this branch merged with main 6cba572f, which includes #6741):

Suite Result
Full pixel-agent JS suite (remote-js-test.sh) tests 3179, pass 3141, fail 0
node --test tests/test-preview-inspection.mjs 24/24
test_preview_inspection.py, no browser 97 run, OK (47 opt-in skips)
test_preview_inspection_distribution.py + test_pixel_inspection_upgrade_dispatch.py 79 passed
ODS_PREVIEW_BROWSER_TESTS=1 test_preview_inspection.py, real Chromium, run twice 90 passed, 7 skipped (Docker image tests), 169 subtests, 0 failed both times

On the merge commit 2dc6b0b4 before the last commit, the browser suite had 1 failed, 90 passed in both runs: #6741's icon-variant expectation described above. GitHub CI on 2dc6b0b4 is green, and its logs show the new compile guard running in both Portal runtime contracts and Test Linux (ok 1247, ok 1248).

Pre-merge head 42493077: JS suite tests 3140, pass 3102, fail 0; browser suite 80 passed, 7 skipped, 137 subtests, 0 failed, twice; no-browser unittest 87 OK; distribution + upgrade dispatch 79 passed.

Branch commits: 448a4bb8 fix, 04b78d7d whitespace parity query, 1216a92d linear cost, 42493077 CI compile guard, 2dc6b0b4 merge of main (#6741), ada56f51 icon-variant expectation and reliability doc.

Red checks. These are the same tests with the capsule swapped out:

Capsule Result
main (d4a61f33) 30 failed. Both replays reproduce the recorded no_match. Also failing: text-transform, uniqueness, hidden descendants, spoofing, parity, and the fake-CDP tests (no ROLE_NAME_RENDERED).
This branch with measure() reverted to the AX-only path 14 failed. Both replays, text-transform, uniqueness, spoofing, and the source-name unit tests.
Rendered name keeps hidden descendants parity and test_source_text_matching_stays_rendered_only fail.
Walk without the hidden-for-ARIA filter killed by the parity test's whitespace-only name query (second commit).
Before the third commit (04b78d7d) test_role_name_matchers_do_linear_work fails: 22,717 style reads against a bound of 4,088, and 303 label reads against 3. Both parity tests pass, so names are unchanged.
Third commit without the hiddenForAria cache the rendered matcher fails the style bound (22,717 reads).
Third commit without the label index both matchers fail the label bound (303 reads).
A duplicated const parentOf in the rules both compile-guard tests fail.
Merged head (2dc6b0b4): no hiddenForAria cache; no label index; duplicated const parentOf each killed the same way: 22,717 style reads; 303 label reads; both compile-guard tests plus a SyntaxError in every real-browser matcher call.

Notes on the rows above:

  • Hidden-descendants mutant: that run also had one test_navigation subprocess timeout under load. It did not reproduce: 3/3 passes each on branch and main, about 1.4 s each.
  • Walk-filter mutant: for any non-empty name it is equivalent, because a hidden element's rendered-only name is empty.

Also run locally on Windows (HeadlessChrome 149, Playwright 1.62.0), on the merged head: both replays give the same results (main no_match, branch passed and visibility_mismatch). The fake-CDP tests (including #6741's ControlNamesTests), both parity tests, the cost test and the compile guard pass there too. Four tests need POSIX APIs (os.getuid, Unix sockets) and ran only on tower2.

Not run: DockerCapsuleTests. They need an image built from this capsule, and none was built on a fleet host.

🤖 Generated with Claude Code

Lightheartdevs and others added 6 commits September 25, 2026 22:07
…s Chromium's transformed names

Chromium's accessibility names apply CSS text-transform. A button whose
source text is "Show sold out", styled uppercase, is named "SHOW SOLD OUT"
in Accessibility.queryAXTree, so an exact click or assert-visible on the
owner's name returned no_match. The capsule's own click (get_by_role
exact) and the owner's getByRole check both use Playwright's source-text
names and see the button.

Fleet laptop round 107 (main d4a61f3) lost website-create on this: the
recorded plan clicked button "Show sold out" on site-1f5f2cf8 (no_match),
then the run hit the no-progress stop. Round 101 hit the same no_match on
site-458e7785 and recovered only by switching to '#revealBtn'.

assert-visible and click now unite Chromium's rendered matches with the
same Playwright-rule matcher assert-hidden already uses, in its
rendered-only form (ROLE_NAME_RENDERED): only elements not hidden for
ARIA (Playwright's isElementHiddenForAria), and hidden descendants left
out of a name. Union by identity, same MAX_RENDERED_MATCHES bound and
uniqueness rules; assert-hidden and the click dispatch are unchanged.
Names stay exact. Matching stays in the isolated world and read-only.

Tests: fake-CDP unit tests for the union, exclusion and uniqueness; opt-in
real Chromium tests for text-transform, hidden-for-ARIA exclusion,
uniqueness, spoofing and parity with Playwright's default getByRole; and
replays of the two recorded laptop pages (byte-exact fixtures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…den element by an empty name

A whitespace-only locator name normalizes to the empty name. The walk
filter keeps hidden elements out even then, as Playwright's default
getByRole does; the parity fixture now checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every role/name assert-visible and click now runs the isolated-world
matcher once per stability sample, inside the 1.5 s observe window. Two
paths in the shared rules were superlinear:

- The rendered name asks hiddenForAria for every descendant, and a
  display:contents element re-walked its children each time, so a
  nested display:contents chain cost depth squared style reads. Results
  are now kept per element per call, like Playwright's cacheIsHidden.
- element.labels makes Chromium scan the element's whole tree on each
  element's first read, so naming N buttons cost N x elements. Each
  tree's labels are now indexed by label.control once per call; the
  result is the same list, in tree order, that element.labels returns.

Both also apply to the assert-hidden matcher. Names and matches are
unchanged: both Playwright parity tests pass with new label cases
(several labels per control, wrapping, a button's label, a label for a
non-labelable element, and a shadow-tree label), and a new browser test
counts style and label reads in the matcher's world: a 200-deep chain
read 22,717 styles before (bound 8 x elements = 4,088; now about 2,500),
and 303 label lists for 303 buttons before (bound 3, the label count).

Measured locally (HeadlessChrome 149): a 3000-card page (18k elements)
first matcher call 579 ms -> 42 ms; the reviewer's 3000 buttons x
20-deep display:contents page went from always 'unstable' to passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capsule's matcher and observation scripts are JavaScript inside
Python strings, and only the opt-in real-browser suite ever executes
them; CI runs neither ods/tests/test_preview_inspection.py nor
ods/tests/test-preview-inspection.mjs. A textual merge that declares a
matcher helper twice (open PR #6741 declares the same consts in the
same block) would make every callFunctionOn throw, so every role/name
step would report 'unavailable' while CI stayed green.

The new pixel-agent test, picked up by the existing tests/*.test.mjs
steps in test-linux and portal-runtime, imports the capsule, compiles
every module-level function script, and runs both role/name matchers on
an empty document to check they keep the passed Chromium matches,
de-duplicated by identity. A duplicated 'const parentOf' fails both
tests with "Identifier 'parentOf' has already been declared".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce-text

#6741 moved the role and name rules into ACCESSIBLE_NAME_RULES, shared
with its load-time CONTROL_NAMES, and ported the same hidden-for-ARIA
rule with its own helper names. The resolution keeps one set of rules:

- main's ACCESSIBLE_NAME_RULES, excluded/outsideTree, reference(o, e,
  kind) with hiddenLabel/hiddenLabelledBy, and SVG <title> names
- this branch's per-call hiddenForAria cache and label index, which now
  also serve CONTROL_NAMES
- ROLE_NAME_MATCH (the exact-name walk) appended to the shared rules
  with renderedOnly false (ROLE_NAME_INCLUDING_HIDDEN) or true
  (ROLE_NAME_RENDERED); git's auto-merge had instead left main's single
  definition carrying this branch's walk, followed by this branch's two
  definitions of a now-missing ROLE_NAME_RULES
- both parity pages keep both sides' cases; the protocol doc's test
  paragraph lists both sides' fixtures
- the compile guard also requires and runs CONTROL_NAMES, and the
  shared-rules test also checks ROLE_NAME_RENDERED

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#6741 recorded, "so a change is noticed", that an exact-name click on
the repaired round-100 page with an aria-hidden icon returned no_match:
Chromium's accessibility name keeps the space after the icon
(" Show sold out") and role/name steps matched only that name. On the
composed tree the rendered matcher also uses Playwright's getByRole
name, which normalizes the whitespace like the load-time control names,
so the click matches the one rendered button and every variant passes.
Found by running ODS_PREVIEW_BROWSER_TESTS=1 on the composed tree
(1 failed, 90 passed before this change, twice).

COMPLETION-RELIABILITY.md now says the load-time "name is on the page"
feedback was written for Chromium-only matching and remains for a page
whose control changed between load and the step. The plugin and its
recorded replays are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant