feat(#37): CDP DOM-read verification path for React/div-soup robustness - #39
Merged
Merged
Conversation
React/div-soup apps expose thin a11y trees, so structural verification has little to assert on even though the app renders fine (issue #37). Touchpoint's CDP seam can read the *actual DOM* (source="dom") for Chromium/Electron/WebView2 targets, catching role-less <div> text the a11y projection omits. Investigation is captured in planning/REACT_ROBUSTNESS.md: exactly what the CDP DOM walk gives us, how it decides a target is CDP-backed, why it fits the ui.py discipline (owned-only via PID, re-resolve fresh, abstain honestly), an honest list of what it does NOT solve (non-Chromium React, no-debug-port apps, canvas, native), and a scoped slice plan. Ships the one safe, small, fully-testable slice - an abstention-correct, read-only DOM-text reader: - DomUnavailable(UIError): new abstention condition, added to ABSTENTION_CONDITIONS (surfaces as CannotVerify, not pass/fail; not an AssertionError subclass). - WindowHandle.read_dom_text(query): resolves a name query against a fresh live-DOM walk of the owned window and returns its DOM text/value. Re-checks ownership first. Abstains DomUnavailable when the target isn't CDP-backed (touchpoint raises TouchpointError/BackendUnavailableError, or the walk is empty) - never false-passes. A readable DOM missing the query still raises the ordinary ElementNotFound (a real "not there"), not an abstention. - _resolve refactored to share _match(els, query, role) between the AX and DOM paths; AX behavior unchanged. Fake-driven tests (tests/test_ui.py::TestDomRead) cover: reading DOM text a thin AX tree omits, abstaining on TouchpointError / BackendUnavailableError / empty walk, ElementNotFound (not abstention) for an absent query, ambiguity, fresh-every-call, ownership re-check after disown, and abstention registration. Deferred to the live acceptance slice (needs a running React sample): proof a real CDP DOM walk returns div-soup content. Full suite green (242 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CameronCrow
force-pushed
the
issue-37-cdp-dom-verification
branch
from
July 23, 2026 15:15
eafb849 to
dd13826
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #37 (investigation + first slice; remaining slices scoped in the doc).
What this is
React/div-soup apps expose thin a11y trees, so structural verification has
little to assert on even though the app renders fine. This PR investigates
touchpoint's CDP seam as the way to close that gap, documents the findings +
a scoped plan, and ships the one safe, small, fully-testable slice.
CDP findings (verified against touchpoint source)
source="dom"path that injects a JS DOMwalker via
Runtime.evaluateand walks the actual live DOM — not the a11yprojection. A role-less
<div>Total: 42</div>with no ARIA becomes an elementnamed
"Total: 42"; shadow DOM is traversed;aria-hiddensubtrees dropped;get_text_contentreads livetextContent/value. This is exactly thecontent a thin React a11y tree omits.
windows()replaces a CDP-owned PID's native window with theCDP page-target window (
cdp:id, browser main PID), so a Cyclaudes handlefor such an app already carries the CDP id — no extra plumbing.
is_ownedancestry), re-resolve fresh (the DOM is re-walked every call), andabstain honestly (touchpoint raises
TouchpointErrorwhen not CDP-backed).What CDP does NOT solve (in the doc, honestly)
Non-Chromium React (React Native, WebKit/Gecko webviews), apps not launched with
--remote-debugging-port, canvas/WebGL content, native div-soup, cross-originiframes. Where none apply, the honest answer stays: abstain + tell the dev to add
ARIA/
data-testidhooks. Never a silent pass.Shipped slice
DomUnavailable(UIError)— new abstention condition (CannotVerify outcome).WindowHandle.read_dom_text(query)— read-only DOM-text reader through thefull discipline; abstains, never false-passes when the target isn't a
readable CDP-backed DOM; ordinary
ElementNotFoundwhen the DOM is readablebut the query is absent.
_resolverefactored to share_matchbetween the AX and DOM paths; AXbehavior unchanged.
Fake-driven tests in
tests/test_ui.py::TestDomRead(9 tests). Full suite green(242 passed, 6
livedeselected). The one thing fakes can't prove — a real CDPDOM walk returning div-soup content — is deferred to the live acceptance slice
(needs a running Chromium React sample), scoped in
planning/REACT_ROBUSTNESS.md.Deliberately NOT built (scoped as slices in the doc)
Settle/retry + DOM-sourced
assert_*(slice 3), andapp_sessionlaunchingtargets with
--remote-debugging-port(slice 4). Kept out to keep this slicesmall, safe, and provable; a false-pass on a React UI would be worse than an
honest "not supported yet."
🤖 Generated with Claude Code