Skip to content

feat(#37): CDP DOM-read verification path for React/div-soup robustness - #39

Merged
CameronCrow merged 1 commit into
mainfrom
issue-37-cdp-dom-verification
Jul 23, 2026
Merged

CameronCrow merged 1 commit into
mainfrom
issue-37-cdp-dom-verification

Conversation

@CameronCrow

Copy link
Copy Markdown
Owner

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)

  • Touchpoint's CDP backend has a source="dom" path that injects a JS DOM
    walker via Runtime.evaluate and walks the actual live DOM — not the a11y
    projection. A role-less <div>Total: 42</div> with no ARIA becomes an element
    named "Total: 42"; shadow DOM is traversed; aria-hidden subtrees dropped;
    get_text_content reads live textContent/value. This is exactly the
    content a thin React a11y tree omits.
  • Touchpoint's windows() replaces a CDP-owned PID's native window with the
    CDP page-target window (cdp: id, browser main PID), so a Cyclaudes handle
    for such an app already carries the CDP id — no extra plumbing.
  • It fits the ui.py discipline cleanly: owned-only via PID (CDP window pid +
    is_owned ancestry), re-resolve fresh (the DOM is re-walked every call), and
    abstain honestly (touchpoint raises TouchpointError when 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-origin
iframes. Where none apply, the honest answer stays: abstain + tell the dev to add
ARIA/data-testid hooks. Never a silent pass.

Shipped slice

  • DomUnavailable(UIError) — new abstention condition (CannotVerify outcome).
  • WindowHandle.read_dom_text(query) — read-only DOM-text reader through the
    full discipline; abstains, never false-passes when the target isn't a
    readable CDP-backed DOM; ordinary ElementNotFound when the DOM is readable
    but the query is absent.
  • _resolve refactored to share _match between the AX and DOM paths; AX
    behavior unchanged.

Fake-driven tests in tests/test_ui.py::TestDomRead (9 tests). Full suite green
(242 passed, 6 live deselected). The one thing fakes can't prove — a real CDP
DOM 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), and app_session launching
targets with --remote-debugging-port (slice 4). Kept out to keep this slice
small, safe, and provable; a false-pass on a React UI would be worse than an
honest "not supported yet."

🤖 Generated with Claude Code

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
CameronCrow force-pushed the issue-37-cdp-dom-verification branch from eafb849 to dd13826 Compare July 23, 2026 15:15
@CameronCrow
CameronCrow merged commit 68d62fb into main Jul 23, 2026
1 check passed
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.

robustness: React/div-soup apps expose thin a11y trees — make verification robust to them

1 participant