TLDR: A synthetic .click() can return successfully and still do absolutely nothing in Radix UI. If your E2E tests drive the browser via CDP eval, you need real pointer-event sequences — not element clicks.

The Setup

We've been building an internal webinar event manager (Next.js, Supabase, shadcn — which layers Radix UI primitives under the hood).

The testing setup: a CLI that drives Arc browser via Chrome DevTools Protocol, letting me run JS evals against the live deployed app without a Playwright config or a test runner. Fast, direct, real browser.

I love it.

And then I hit the wall.

The Wall

The assignee dropdown on the task list would not open.

My eval was something like:

document.querySelector('[data-testid="assignee-trigger"]').click()

No error. No exception. Eval returned fine. The dropdown stayed closed.

I tried dispatchEvent(new MouseEvent('click', {bubbles: true})). Same result. Silence.

(This commit message tells the story pretty cleanly: fix(tasks): assignee dropdown actually opens.)

What's Actually Happening

Here's the thing about Radix — its interactive primitives don't listen for click. They listen for pointer events. Under the hood, Radix uses pointerdown/pointerup sequences to manage state, handle outside-clicks, and decide when a menu should open or close.

A synthetic .click() bypasses that entirely. The element receives the event, nothing in Radix's listener stack fires, and the dropdown stays exactly where it was.

The fix is a real pointer pair:

const el = document.querySelector('[data-testid="assignee-trigger"]')
el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, isPrimary: true }))
el.dispatchEvent(new PointerEvent('pointerup',   { bubbles: true, isPrimary: true }))

That opens it.

And to select an item once it's open — there's a second trap. You can't just pointerdown+pointerup on the option. Radix needs to register it as hovered first:

item.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
item.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, isPrimary: true }))
item.dispatchEvent(new PointerEvent('pointerup',   { bubbles: true, isPrimary: true }))

Without the pointermove, the value never commits. The item just… doesn't register.

The Second Trap (This One Hurt More)

While testing the inline task title edit, I dispatched new Event('blur') to simulate the user clicking away and committing the change.

The eval ran clean. No error. I checked the DOM — input was gone, looked committed.

It wasn't persisted. At all.

React 17+ delegates onBlur to the focusout event under the hood. A plain blur event fires into the void. The React handler never sees it. So I had a test that exercised the path that gets skipped and called it a pass.

Fix: new FocusEvent('focusout', { bubbles: true }). Or just dispatch Enter on the input and use the keyboard commit path if the component has one (it did).

The Verify Discipline (Why This Matters)

This is the part I want every builder — and every AI agent driving a browser — to internalize:

A synthetic interaction succeeding doesn't mean the feature works for a real user.

Before trusting any CDP-driven test result on a widget, check two things:

  1. getComputedStyle(element).pointerEvents !== 'none' — the element can actually receive real pointer input
  2. document.elementFromPoint(x, y) at the element's center lands where you think it does — not on an overlay

If either of those is wrong, your synthetic click will succeed and your user will be stuck.

The test suite passed. The assignee dropdown was broken in prod. That's the lesson.