Momentic
Back to Blog

Hot libraries and heartbreak: Struggling with Cursor, RTL, and shadcn

Why testing libraries can be a real pain to use.

Writing unit tests for a wildly popular component library like shadcn/Radix UI with React Testing Library (the most popular testing framework in the React ecosystem) feels like it should be in Cursor’s wheelhouse. Like, you should be able to describe some test cases and hand off to an agent and call it a day. The reality is disappointingly complicated 😓. Here’s what feels like should be a straightforward task: testing the behavior of a popover inside a dialog.

popover

Here’s what Claude comes back with given the component above:

import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
 
import { DialogWithPopover } from "./DialogWithPopover";
 
it("displays popover inside a dialog", async () => {
  render(<DialogWithPopover />);
 
  // Open the dialog
  await userEvent.click(screen.getByTestId("dialog-trigger"));
 
  // Wait for the dialog content to appear (portalled, so query from document.body)
  const dialogContent = within(document.body).getByTestId("dialog-content");
  expect(dialogContent).toBeInTheDocument();
 
  // Open the popover inside the dialog
  const popoverTrigger = await screen.findByTestId("popover-trigger");
  await userEvent.click(popoverTrigger);
 
  // Wait for the popover content to appear (portalled, so query from document.body)
  const popoverContent = within(document.body).getByTestId("popover-content");
  expect(popoverContent).toBeInTheDocument();
});

Nothing out of place at first glance, just trying to validate the behavior shown above. And yet, even despite the effort to accommodate the popover’s content being portalled, we run into the error below:

TestingLibraryElementError: Unable to find an element by: [data-testid="dialog-content"]

You’re gonna have to trust me that the test-ids aren’t the culprit here (or check the sandbox).

It's not the tool's fault

This isn't meant as a knock against agentic coding tools (if they’re slowing you down, you’re probably offloading the wrong tasks). Nor is there really anything “wrong” with shadcn, Radix UI, or React Testing Library. As Andy Hook, one of the maintainers of Radix UI, observes it’s kind of a death by a thousand cuts situation:

There are quite a few gotchas. Especially common are pointer event firing problems and mocking layout-specific browser APIs, as JSDOM struggles with these…

I’ve found when writing unit tests to cover pretty much anything that touches @radix-ui/react-dismissable-layer (i.e., tooltips, popovers, dialogs) with React Testing Library, sooner or later, you’re gonna have a bad time.

When fixing one thing breaks another...

For example, in trying to prevent a dialog from closing on escape key down when an internal popover is open (clip below), I needed to update to @radix-ui/react-popover@1.1.2.

Expected escape key down behaviorActual escape key down behavior
expectedactual

Sadly, this broke several of our unit tests as @radix-ui/react-popover@1.1.2 began setting pointer-events: none on document.body when the dialog is active. So any test involving dialogs and userEvent.Click now gave rise to the error:

"Unable to perform pointer interaction as the element has pointer-events: none"

userEvent.click from @testing-library/user-event aims to simulate a realistic click, but things kind of go sideways here with JSDOM. When Radix sets pointer-events: none on the <body>, it’s effectively telling the entire page to stop accepting pointer interactions. In a real browser, that’s fine — if you have a child element, like a dialog with a button inside it, that explicitly sets pointer-events: auto, the browser knows to let that button’s click event through: the hit-testing engine walks up the visual stack, understands the CSS cascade, and respects that the child has opted back in to pointer events. But JSDOM doesn’t do any of that. It doesn’t simulate visual hit-testing. It doesn’t walk the stack. It just asks for the computed style of the element you’re clicking — and sometimes, even if the child element’s parent has pointer-events: auto, JSDOM will stubbornly report pointer-events: none because it’s blindly inheriting the body’s setting without properly processing overrides.

I ended up adopting the unsatisfactory workaround below:

const userEventWithPointerEventsCheckDisabled = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never });

Disabling the pointer events check isn’t really solving the problem — it’s just telling the test to stop caring whether the element is actually clickable. We’re trading test accuracy for test passibility 🙁.

Are these types of tests even worth it?

The somewhat ironic part of using JSDOM for pointer-event interactions is that the author of React Testing Library, Kent C. Dodds, writes “The more your tests resemble the way your software is used, the more confidence they can give you.” The more contortions we have to make to accommodate the quirks of JSDOM, the further we get from an environment representative of real use, the less confidence we have that our tests really hold water.

I’m not saying ditch unit tests altogether — pretty much anything that doesn’t involve event-driven input (i.e., user interaction) seems like fair game. But if you’re trying to test interaction, honestly, you’re better off reaching for end-to-end tests or tools like Momentic. They’re just a better fit — you’re no longer stuck bending tests to accommodate virtualized pointer events into something vaguely testable, which, let’s be real, was never going to end well anyway.

Accelerate your team with AI testing.

Book a demo