skip to content

A Testing Library test clicks a button with fireEvent.click and passes, but in the browser that button does nothing because a CSS rule sets pointer-events: none on it. Why did the test still pass, and what would user-event do differently?

level: seniorimportance: should knowfreq 43%

answer

  1. dispatchEvent asks nobody for permission
  2. the browser decides before the event exists
  3. pointer-events is a style check, not geometry
  4. jsdom has no layout, so no hit-testing

basics

~20 s

fireEvent calls dispatchEvent directly, skipping everything the browser decides before an event exists — so it clicks elements no user could reach. user-event checks the element's effective pointer-events and throws instead of clicking, turning the silent pass into a failure.

solid answer

~50 s

A real click starts with the browser deciding whether the element can be interacted with: it hit-tests, honours `pointer-events`, and suppresses interaction on disabled form controls. `fireEvent.click` starts *after* that decision — it constructs an event and dispatches it at the node you hand it, so an unreachable element gets clicked and the handler runs. That is a false positive: the test asserts a feature works when the user cannot invoke it. `user-event` models the interaction rather than the event, so before clicking it resolves the effective `pointer-events` for the element and its ancestors and throws if the answer is `none`. The honest caveat is that jsdom has no layout engine, so neither API can detect an element covered by an overlay or scrolled out of view — that class of unclickability needs a real browser.

go deeper

for a junior

Know that dispatching an event bypasses whatever the browser would check first, so a test can click something a user cannot, and that the interaction API is the safer default.

for a middle

Explain the mechanism: dispatchEvent delivers to the node unconditionally, while user-event resolves effective pointer-events across ancestors and throws rather than clicking.

for a senior

Frame it as a false-positive risk in the suite, and state the boundary precisely — style-level unreachability is catchable in jsdom, layout-level occlusion is not and needs a browser test.

for a principal

Decide where the guarantee should live: how much reachability confidence belongs in component tests versus a thin browser smoke suite, and what that split costs in runtime and maintenance.

## Where the two APIs enter the story A click in a browser has two halves. First the browser decides **whether an interaction is possible at all**: it hit-tests the pointer coordinates against the rendered box tree, respects the effective `pointer-events` value, and refuses to route interaction to disabled form controls. Only if that succeeds does the second half happen — the ordered burst of pointer, mouse and focus events that ends in `click`. `fireEvent.click(element)` enters at the second half, and only at its last step. It builds one `MouseEvent` and calls `dispatchEvent`. `dispatchEvent` is a DOM primitive with no notion of reachability — it delivers the event to the node you named and runs its listeners. That is why the test passed: the code under test never learned that the button was unclickable, because nothing in the path checked. ## Why this is worse than a merely weak test The failure mode here is not "the test covers less", it is **the test is actively misleading**. Green means "clicking Add to cart adds the item", the feature is broken in production, and the suite defends the broken state — if someone later fixes the styling, no test changes. Regressions in reachability, which are common because they come from CSS and layering rather than from component code, are invisible to a suite built on raw dispatch. ## What user-event checks Before performing a pointer interaction, user-event resolves the effective `pointer-events` for the target and its ancestors — the property inherits, so a container set to `none` disables its children — and if the result is `none` it throws an error rather than dispatching. The check runs per API call by default and can be relaxed through a `pointerEventsCheck` option on `userEvent.setup()` when you deliberately need it out of the way. Disabled form controls get similar treatment: user-event will not deliver a pointer interaction to a disabled `<button>` or `<input>`, matching what a browser does. ```javascript const user = userEvent.setup() await user.click(button) // throws when the effective pointer-events is none ``` ## An honest nuance about disabled buttons in React It is worth knowing that `fireEvent.click` on a genuinely `disabled` React `<button>` usually does *not* call `onClick` — React suppresses mouse-event handlers on disabled form controls, so a test that asserts "nothing happens" passes for a second reason. The gap opens up wherever nothing suppresses the event: a `<div role="button">` carrying `aria-disabled="true"` with its handler still attached, a custom control that only *looks* disabled, or exactly the `pointer-events: none` case above. Those are also the cases most likely to be genuinely broken, because the developer relied on styling rather than on semantics. ## What no jsdom-based test can catch Be precise about the limits, because overclaiming here is a common interview stumble. jsdom does not lay out or paint anything: every element has zero dimensions and there is no stacking or hit-testing. So user-event's check is a **style** check, not a geometry check. It cannot tell you that: - a full-screen modal overlay is sitting on top of the button, - the button is scrolled outside the viewport, - another element with a higher `z-index` intercepts the pointer, - the button is rendered at zero size or behind a `transform`. Those belong to a real-browser end-to-end test, whose driver performs actual hit-testing before acting. This is one of the clearest illustrations of what the levels of the test suite are for: a component test can prove the wiring and the styling intent; only a browser can prove the pixel is clickable. ## How to answer the diagnosis question A good answer moves in three steps. Explain the mechanism — dispatch bypasses the browser's interactivity decision. Name the fix — use the interaction-level API so the test fails loudly on the unreachable element, and assert on user-visible outcomes rather than on a handler spy, so a click that cannot happen cannot be papered over. Then state the boundary — the class of unclickability that comes from layout needs a real browser, and if this bug class keeps recurring, that is an argument for a thin end-to-end smoke test over the critical path rather than for more elaborate unit-level mocking.

  • Would switching the assertion from a handler spy to a DOM outcome have caught this bug?
    Not on its own. With fireEvent the handler still runs, so the cart badge would still update and the DOM assertion would pass too. Asserting on outcomes is the right habit for other reasons, but the fix for this bug is the interaction-level API that refuses to click an unreachable element.
  • Can a jsdom test tell you a button is hidden behind a modal overlay?
    No. jsdom performs no layout, so nothing has a position or size and no element can be 'on top of' another. Only a real browser driver hit-tests coordinates before acting. If overlay regressions matter, cover the critical path with a browser-based test rather than trying to simulate geometry.
  • When would you deliberately turn user-event's pointer-events check off?
    Rarely, and only for a known jsdom limitation — for example a third-party component whose styles do not resolve meaningfully outside a browser. You would scope the relaxed setup to that test with an explicit comment, because a globally disabled check quietly returns the whole suite to fireEvent-grade fidelity.
  • Why is aria-disabled a bigger risk here than the disabled attribute?
    Because `disabled` is enforced by the platform — the control is genuinely inert — while `aria-disabled` is a hint to assistive technology only. The handler stays attached and the element stays clickable unless the code also refuses the action, so a test that dispatches an event will happily invoke behaviour the UI claims is unavailable.

saying these in an interview costs you the question

  • Claims user-event detects elements hidden behind an overlay
  • Thinks jsdom performs hit-testing or layout
  • Believes dispatchEvent respects pointer-events or disabled state
  • Treats aria-disabled as making a control genuinely inert
  • Concludes the fix is to assert on the handler spy instead

context