A test clicks Save. The app saves the record. A green toast appears, hangs around for two seconds, and blocks the next button.
The test fails.
Someone adds a three-second sleep. The test passes. Everyone moves on.
Two weeks later, the toast takes four seconds to disappear on a busy CI machine. Same failure. Different Monday.
I think this is one of the most expensive habits in frontend testing: treating every unpredictable UI event as a timing problem. Sometimes the timing is fine. The application has changed which element is actually available for interaction.
A small UI change can be a big state change
Modern apps throw a surprising amount of stuff at a browser: notifications, loading masks, drawers, dropdowns, permission prompts, confirmation dialogs, and third-party widgets. Any of these can alter where focus goes or which control is on top.
Imagine this sequence:
- A user changes a billing address.
- A confirmation dialog opens.
- Clicking Confirm triggers a loading overlay.
- A toast slides across the bottom of the page.
- The test tries to click Continue.
If Continue is behind the toast, clicking it isn't a legitimate user action at that moment. Retrying the same coordinates is no better. A useful test should wait for an actionable state, then locate the button again.
This is why I like the idea behind this benchmark for testing toast interference and modal stacking. It separates completing the flow from recovering sensibly when the interface gets noisy. Those are different abilities.
A test that passes after twenty silent retries isn't automatically better than one that fails immediately with a screenshot showing the obstruction.
The DOM can be ready while the experience isn't
React and Next.js introduce a variation of the same problem. The server sends HTML. The browser paints it. Hydration begins. Something changes.
The button you found on the first render might be replaced, disabled, or wired to behavior only after hydration. This creates a particularly irritating failure: everything looks fine in the screenshot, but the click happens at the wrong stage.
Blanket rules such as "fail the build for every hydration warning" are tempting. They also make it hard to distinguish a harmless discrepancy from a broken purchase button.
A more useful approach is to reproduce the mismatch, identify the affected component, and assert the interaction the user actually needs. There's a focused walkthrough of testing React and Next.js hydration mismatches without treating every warning as fatal.
The key question isn't whether the console is immaculate. It's whether the page ended up usable and correct.
Focus bugs are the quiet cousin of click bugs
Custom comboboxes are a great trap for overconfident tests. You type into the box, see an option, click it, and assume the widget works.
Now try the same thing without touching the mouse.
Does Arrow Down move the active option? Does Escape close the list? Does focus remain where it should? Does aria-activedescendant point to a real option? Does Enter choose the highlighted item rather than the first item in the DOM?
A click-only test can happily pass through a component that fails for keyboard users. The combobox and typeahead testing guide is worth using as a checklist when you're dealing with custom widgets.
I'd add one rule: check the resulting value and focus state, not merely whether a dropdown was visible.
Please stop screenshotting every pixel
The same mistaken obsession with implementation details shows up in layout testing.
A grid can reflow because content grows, a font changes, or the viewport narrows. That doesn't necessarily mean the user experience is broken. Meanwhile, a footer button can become inaccessible with only a tiny visible change.
For CSS subgrid and intrinsic sizing, I'd rather assert things like:
- Key elements don't overlap.
- Important controls remain inside the viewport or reachable by scrolling.
- Text isn't clipped.
- Reading and tab order still make sense.
Use screenshots for visually meaningful regressions, not as the only definition of correctness. The practical examples in this piece on testing CSS subgrid and layout reflows make that distinction nicely.
A better way to debug the next "flaky" failure
When the next intermittent click failure appears, resist the immediate urge to increase a timeout. First find out what was actually on screen. Was the button covered? Was it disabled? Did the page re-render? Where was keyboard focus? Which state was the app in immediately before the action?
Then create a small, repeatable version of the problem. Force the toast to appear. Delay the hydration step. Stack the dialogs. Navigate with the keyboard.
A test that fails consistently under a known bad state is easier to fix than a test that passes inconsistently after a random sleep.
The funny part is that teams often spend more time arguing about locator syntax than asking whether the element was genuinely available to the user. The second question is usually where the bug lives.
Top comments (0)