Skip to content

feat: CDPBrowser helper family — Obscura and Kitesurf mini-browser support - #5681

Open
DavertMik wants to merge 21 commits into
4.xfrom
feature/cdp-browser-helpers
Open

feat: CDPBrowser helper family — Obscura and Kitesurf mini-browser support#5681
DavertMik wants to merge 21 commits into
4.xfrom
feature/cdp-browser-helpers

Conversation

@DavertMik

@DavertMik DavertMik commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Motivation/Description of the PR

A new class of lightweight, agent-era browsers (Obscura, Cloudflare Kitesurf) speaks CDP but breaks under Puppeteer/Playwright's private machinery (isolated worlds, injected query handlers, actionability checks). This PR adds a raw-CDP helper family that drives any CDP endpoint with one in-page round-trip per action — no element handles, no stale-context failures:

  • CDPBrowser — abstract base helper (~100 actions: navigation, semantic locators, assertions, grabbers, waits, cookies, screenshots, scrolls, CSS/attribute checks, keyboard/mouse basics). Transport is a minimal ws-based CDP client; semantic by-text/by-label locators run through an injected in-page client plus an XPath polyfill (built from our existing xpath dependency with isHtml: true) for browsers with partial XPath engines. Capability flags (layout, xpath, screenshot) make unsupported actions fail with clear messages instead of protocol noise.
  • Obscura — preset subclass for the Obscura mini-browser: synthetic input (click dispatches in-page el.click()), XPath polyfill on, screenshots/visibility assertions rejected with actionable errors, optional binaryPath to auto-spawn obscura serve (hardened: fail-fast on bad path, SIGTERM→SIGKILL escalation).
  • Kitesurf — preset subclass for Cloudflare Browser Run (beta): session acquire/release via the CF API, Bearer-auth WebSocket, CDP input on real layout, screenshots. Tests self-skip without CF_ACCOUNT_ID/CF_API_TOKEN.
  • Canonical shared spec: the CDP helpers run the same test/helper/webapi.js suite as Puppeteer/Playwright/WebDriver — no parallel spec. isHelper() now walks the prototype chain so isHelper('CDPBrowser') covers subclasses; unsupported tests are marked with this.skip() so the pending list shows true coverage. Tallies: Chrome 185 passing / 128 pending, Obscura 108 passing / 193 pending, 0 failing. Skips are real limitations only (iframes, popups, file upload, drag&drop, rich-text editors, strict mode; plus layout/screenshot gates and 3 documented upstream DOM bugs on Obscura). Existing helpers are unaffected — the Puppeteer slice was verified byte-identical against unmodified webapi.js.
  • CI: obscura.yml workflow — binary version- and SHA-256-pinned (verified on every run incl. cache hits), permissions: contents: read, persist-credentials: false. Kitesurf CI intentionally omitted until the promised open-source binary ships.
  • Docs: docs/alternative-browsers.md (when these engines beat Playwright: CI smoke speed, DOM-not-pixels testing, and near-unlimited parallel scale with Kitesurf + run-workers — each worker auto-acquires its own cloud browser), plus generated helper reference pages.
  • Also includes three small Puppeteer helper fixes from the research phase: tolerate missing Page.bringToFront (previously hung the runner after printing OK), refresh context on framenavigated, read innerText via evaluate instead of getProperty.

Based on the empirical research in plans/006 (session research doc, kept untracked). Reviewer note: the CI workflow downloads and executes the Obscura release binary — the SHA-256 pin freezes the artifact (trust-on-first-use from the audited v0.1.11 release); please eyeball that step when reviewing. Squash-merge recommended (one intermediate commit briefly tracked a local plans file).

Applicable helpers:

  • CDPBrowser (new)
  • Obscura (new)
  • Kitesurf (new)
  • Puppeteer
  • Playwright
  • WebDriver
  • REST
  • FileHelper
  • Appium

Applicable plugins:

  • aiTrace
  • autoDelay
  • autoLogin
  • customLocator
  • pause
  • coverage
  • heal
  • retryFailedStep
  • screenshot
  • selenoid
  • stepTimeout
  • subtitles

Type of change

  • 🔥 Breaking changes
  • 🚀 New functionality
  • 🐛 Bug fix
  • 🧹 Chore
  • 📋 Documentation changes/updates
  • ♨️ Hot fix
  • 🔨 Markdown files fix - not related to source code
  • 💅 Polish code

Checklist:

  • Tests have been added (unit: CDPConnection + client script; helper suites vs real Chrome and live Obscura; shared cross-helper spec)
  • Documentation has been added (Run npm run docs)
  • Lint checking (Run npm run lint)
  • Local tests are passed (Run npm test) — unit suite 779 passed, 0 failed, 11 skipped

🤖 Generated with Claude Code

DavertMik and others added 21 commits August 7, 2026 00:53
…menavigated, read innerText via evaluate

Prevents runner hang when CDP endpoint lacks Page.bringToFront (startup
aborted before isRunning=true, teardown skipped, open socket kept node
alive). Context also refreshes on framenavigated for endpoints that do
not emit Page.loadEventFired on click navigations. Text extraction via
evaluate avoids getProperty remote-object round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Wrap JSON.parse and listener invocations in try/catch to prevent
  uncaught exceptions from crashing the process or preventing other
  listeners from firing
- Guard send() to reject immediately if connection not open, preventing
  timer leaks when called before connect() or after close()
- Add test cases covering both fixes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…luate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ution

Global fetch is unavailable on Node 16, which package.json's engines field
still supports. Every other HTTP-calling helper (REST.js, GraphQL.js,
ApiDataFactory.js) already depends on axios, so _resolveEndpoint now uses
it instead of relying on a Node 18+ global.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, fix screenshot test output dir

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Attach an error listener to the spawned obscura process so a bad
binaryPath surfaces as a clear _connect rejection instead of an
uncaught exception, and make _finishTest escalate SIGTERM to SIGKILL
after 5s (confirming actual exit, not just that the signal was sent)
so a SIGTERM-ignoring child can never keep the event loop alive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lans/006

Kitesurf's pre-existing example.com tests used amOnPage('/'), which broke
once the instance url could point at a SITE_URL tunnel instead. Switch
them to absolute URLs so they pass regardless of the configured base url.

plans/ is intentionally untracked in this repo; drop plans/006 from the
index (content unchanged on disk) to keep that convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kitesurf CI intentionally omitted until the promised open-source
binary ships; the Kitesurf spec self-skips without CF credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ons, docs corrections, scroll-into-view clicks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the CDPBrowser actions the canonical webapi.js shared spec exercises
that were implementable in a few lines on top of the existing
_run/_candidates/_evaluate machinery: wait, dontSeeInSource, dontSeeInTitle,
seeCurrentUrlEquals/dontSeeCurrentUrlEquals, seeCurrentPathEquals/
dontSeeCurrentPathEquals, waitUrlEquals, waitCurrentPathEquals,
seeInField/dontSeeInField, grabNumberOfVisibleElements/
seeNumberOfVisibleElements, grabPageScrollPosition/scrollPageToTop/
scrollPageToBottom/scrollTo, grabCssPropertyFrom(All), seeCssPropertiesOnElements,
seeAttributesOnElements, focus/blur, type, resizeWindow, doubleClick/
rightClick/clickXY, waitForVisible/waitForInvisible/waitToHide/waitForDetached,
seeCookie/dontSeeCookie/waitForCookie, grabHTMLFrom(All), executeAsyncScript,
and saveElementScreenshot.

The client script's fill/append/clear/values actions now handle
contenteditable elements (checked via the contenteditable attribute, not
just the isContentEditable property, since Obscura's minimal DOM doesn't
compute it) alongside <input>/<textarea>/<select>. select gained native
multi-select support and a fallback for ARIA role="combobox"/role="listbox"
widgets (click the trigger if present, then click the matching
[role="option"]).

Also fixes three small pre-existing bugs surfaced while wiring the shared
spec:
- waitInUrl's timeout error message used the raw, unresolved urlPart
  instead of resolving it against options.url.
- selectOption always did String(option), so an array value for a
  multi-select collapsed into one unmatchable comma-joined string.
- see/dontSee ignored store.currentStep.opts.ignoreCase and didn't
  normalize whitespace, unlike every other browser helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… instead of parallel spec

CDPBrowser_chrome_test.js, CDPBrowser_obscura_test.js, and
CDPBrowser_kitesurf_test.js now wire into test/helper/webapi.js's tests(),
the same shared spec Puppeteer/Playwright/WebDriver run, instead of the
rejected parallel spec test/helper/cdpwebapi.js (deleted). Kitesurf's file
already had its own its covering the 3 public-page scenarios that used to
live in cdpwebapi.publicTests(), so nothing needed to be duplicated;
webApiTests.tests() is gated on the SITE_URL tunnel condition exactly as
cdpwebapi.tests() was.

webapi.js's isHelper() now walks the constructor's prototype chain instead
of a single constructor.name check, so isHelper('CDPBrowser') matches
CDPBrowser, Obscura, and Kitesurf, while isHelper('Obscura') still matches
only Obscura. Tests CDPBrowser can't run (or, narrower, that Obscura's lack
of a layout/rendering/focus engine can't run) are guarded with this.skip()
so they show as pending -- visible coverage -- instead of silently missing.
Rich text editor tests (ProseMirror, Quill, CKEditor 5, etc.) are skipped
for the whole CDP helper family per explicit direction: they need real
focus/selection/beforeinput semantics a raw-CDP helper intentionally does
not implement. No existing Playwright/WebDriver/Puppeteer guard was touched.

Final tallies: Chrome 185 passing/128 pending/0 failing, Obscura
108/193/0, Kitesurf 0/4/0 (skip-clean, no CF creds). The Puppeteer #see
regression slice (71/1/4) was verified byte-identical against the
unmodified webapi.js -- the 4 failures are pre-existing environment
issues (a hardcoded 'http://localhost' cookie domain vs this env's
127.0.0.1 SITE_URL, and codecept.io content drift), not caused by this
change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…semantics, regenerate docs

- cdpBrowserClient.js: cssProps now reads only the requested properties
  (mirroring the attrsMap pattern) instead of serializing the entire
  ~1090-key/~35KB computed style object per element. CDPBrowser.js's
  grabCssPropertyFrom(All)/seeCssPropertiesOnElements now pass the
  camelCase-resolved property names through the payload.

- _seeInField's boolean branch asserted field *existence*
  (values.length > 0) instead of checkbox *checked state*, so
  I.seeInField(locator, false) silently passed as long as the field
  existed, regardless of whether it was checked. Now asserts the
  checkbox's checked state via the existing 'checked' client action,
  mirroring Puppeteer's proceedSeeInField boolean semantics: true passes
  iff checked, false passes iff unchecked. No webapi.js test currently
  exercises seeInField/dontSeeInField with a boolean value, so this does
  not change either covering suite's tallies.

- waitForCookie passed a literal 'placeholder' message to _poll and
  rewrote it in a catch block that also swallowed any real transport
  error from _poll/grabCookies. Now passes the real timeout message
  directly and the try/catch is gone.

- Regenerated docs/helpers/CDPBrowser.md (npm run docs), which was still
  missing all ~40 methods added when CDPBrowser was wired to the shared
  webapi.js spec. Obscura.md/Kitesurf.md are unchanged (they only document
  methods declared directly on those classes).

Chrome: 185 passing / 128 pending / 0 failing (unchanged).
Obscura: 108 passing / 193 pending / 0 failing (unchanged).

Co-Authored-By: Claude Fable 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