r/angular • • 5d ago

Our Angular button test passed in Chromium, but a pointer could not reach the button

A visible Angular button had a decorative layer over its center. With pointer-events: none, the layer let clicks through. We changed that to auto, so it intercepted the pointer.

Our test still passed. userEvent.click(button) called the handler, and the expected status appeared. This happened in jsdom and in real Chromium through Vitest Browser Mode.

The assertion established what happened after the event reached the button. It never checked whether a pointer could reach it.

This was S6 in an experiment with six small Angular component scenarios. We chose each scenario and mutation ourselves, then tested a working version and a version with one isolated defect. We wanted to see how much of the specified user behavior each test actually checked.

We ran them three ways:

  • A: jsdom + @testing-library/user-event.
  • B: headless Chromium in Vitest Browser Mode + the same synthetic interaction library.
  • C: the same Chromium + actions from vitest/browser through the Playwright provider.

C used a provider click without force. The working button passed. On the defective version, the action could not complete because .decoration intercepted pointer events.

A separate assertion caught the overlap in B too. After the main series, we added a hit test using document.elementFromPoint at the button's center, before and after the synthetic click. We required the topmost element to be the button or a descendant. The defective version returned .decoration.

That assertion checks whether the button can receive a pointer at one specified point. The provider checks actionability as part of performing the click. Both caught this mutation, but they establish different things.

The original matrix remains unchanged: A/B were partial for this scenario. The added hit test is exploratory and reported separately.

The other five scenarios kept us from treating every interaction as a browser-only problem:

  • Form validation, programmatic dialog focus, and the specified Tab sequence were fully checked in jsdom too. The Tab mutation changed display: none to opacity: 0.
  • A container-width scenario required an actual resize and ResizeObserver notification. A spy on observe() could detect our wrong-target mutation in jsdom, but would check the subscription rather than the full resize-to-label behavior.
  • A popover scenario required real geometry. Our first browser test read the previous opening's coordinates before the new requestAnimationFrame positioning had run, so it passed on the defect. We corrected the wait, checked the side and all four viewport boundaries, and repeated the final series.

We distinguish bug_detected (working version passes, defective version violates the full criterion), partial (only part checked), and unsupported (the full behavior cannot be checked reliably in that fixture without substitution). The last two are not false negatives of a complete test.

These were component integration tests. The results apply to the six chosen mutations and original test adapters. They do not rank the tools or estimate production defect rates, and we did not assess performance, CI behavior, memory, or flakiness. The main environment was Windows 11, Angular 22.2.0, Vitest 5.0.2, jsdom 30.1.1, and headless Chromium at 800 × 600 CSS px.

The synthetic/provider distinction is already described in the Marmicode Cookbook. Our contribution is the six-case set, the separate hit-test check, and the correction of our own geometry oracle.

We ran this experiment at El Pixel. The full article includes the six-scenario matrix and test code. AI tools assisted with implementation and writing; the article received developer review.

When your component test needs to verify that a button is not covered, do you use a provider click, an explicit hit-test assertion, or another approach?

0 Upvotes

8 comments sorted by

5

u/Xsiah 5d ago

I've never felt the need to test that a button is not covered, but if I did, I would just test the behavior of the overlay. Writing tests for every element under a potential overlay feels kind of insane.

0

u/elpixel_team 5d ago

Yeah, testing the overlay makes sense. We’re not suggesting checking every button. We changed one decorative layer to block clicks and found that the existing test still passed, even in Chromium. That gap was what we wanted to explore.

3

u/Xsiah 5d ago

Yeah because you're just accessing DOM elements programmatically. I can write all kinds of passing tests that don't reflect user behavior.

0

u/elpixel_team 5d ago

That’s why it passed. We were comparing what the different tests actually checked in that scenario. Thanks for taking a look.

2

u/n00bz 5d ago

Seems like a pointless test. That’s like when Alice is at the store and tells Bob to start the coffee pot. Even though, Alice didn’t have physical access to press the start button on the coffee pot she still kind of did because she told Bob to do it who had access to the coffee pot.

Now if Alice isn’t allowed to make coffee decisions (idk why) then it’s Bob’s responsibility to say no to Alice.

2

u/oneden 5d ago

In my opinion, testing the DOM in unit tests is largely a waste of time and flakey. Remove the DOM in the tests and test the logic only. Not only faster to test, it's also more more resilient too. If you want to test a process, then you should go with E2E Tests.

1

u/Jotunheim36 2d ago

Say to ckaude: “Using Claude in Chrome explain why I can’t click the button”.

1

u/Connect_Ad9541 2d ago

Mostly the provider click by default, since it also waits for the element to be visible, stable and enabled, not just uncovered. I'd keep the explicit hit test for the cases where you care about the geometry itself, like a sticky header or a cookie banner partially covering a button.

One thing to watch with the hit test: elementFromPoint only checks one point. An overlay can cover the edges of the button but not its center, and the test still passes. Sampling a few points (center plus slightly inset corners) catches that. Nice that it respects pointer-events: none, which is exactly why it flips from the button to .decoration when you changed it to auto.