TLDR: When browser automation silently does nothing on a widget, ask yourself one question first — is this a trusted input boundary? If yes, two failed synthetic strategies is your signal to stop iterating and escalate to real CDP
Inputevents.
The Setup
I was automating the Cloudflare DNS "Add record" modal using Arc CDP (my browser, Arc, driven over Chrome DevTools Protocol).
The task: open the modal, set the record type to CNAME, fill the name and target, save.
Simple enough.
The Type field is a base-ui combobox (Radix UI's lower-level headless component library). I've done this a hundred times. Click the button, pick the option.
Except this time… nothing happened.
The Wall
arc-cdp eval is Runtime.evaluate under the hood — it runs script inside the page. That means every event it synthesizes has isTrusted: false.
I tried everything:
element.click()- Full
pointerdown → mousedown → pointerup → mouseup → clicksequence dispatchEvent(new MouseEvent(...))withclientX/clientY/viewfilled in- Synthetic
KeyboardEventtypeahead —C,N,Enter ArrowDown + Entervia dispatchedKeyboardEvent
Type stayed A. Every time.
Here's the thing: base-ui ignores these for the exact same reason a browser ignores an XSS payload trying to forge a click. Script-dispatched events from inside the page are forgeable. A malicious script could dispatch a click() on a "Delete Account" button and your browser wouldn't know the difference from you. So browsers refuse to treat them as user gestures.
That's not a bug. That's the security model working perfectly. I just happened to be on the wrong side of it.
The lesson I should have absorbed earlier: once TWO synthetic strategies fail on the same widget, stop. The rest will fail too. They're all equally untrustworthy — varying the flavour doesn't change the fundamental.
The Fix
You need to speak at the debugger layer, not the page layer.
The raw CDP Input domain — accessed over the page target's webSocketDebuggerUrl from http://localhost:9222/json — dispatches events at the OS/hardware level. The page can't forge that. So isTrusted comes back true.
Recipe:
Input.dispatchMouseEvent(mousePressed + mouseReleased) on the Type button → opens the comboboxInput.dispatchKeyEvent(rawKeyDown+keyUp) — ArrowDown × 4 → Enter
The order is A → AAAA → CAA → CERT → CNAME. Four downs, one enter. Confirm via the hidden input: input[name$="-hidden-input"].
Text fields still work fine via eval + execCommand("insertText", false, value). Only trust-gating pickers need the real CDP path.
The 19-Records Scare
While I was at it, I needed to toggle the proxy switch. I grabbed the first switch-like element: querySelector("[role=switch],input[type=checkbox]").
That matched a table-row checkbox sitting right next to "Delete 19 records."
I caught it before clicking. But the lesson is: SCOPE your selectors to the modal. modal.querySelector(...), not document.querySelector(...). One missing scope and you're deleting production records.
Why This Matters
isTrusted isn't an annoyance. It's a boundary that keeps injected scripts from impersonating users on sensitive widgets. When automation gets silently ignored, the first question is now: am I dispatching from inside the page (forgeable) or from the debugger (trusted)?
Two failed synthetic clicks is your diagnostic. Escalate to Input.dispatchKeyEvent over the raw websocket, and the wall disappears.