Fix Cypress Flaky Tests: Top Code Smells

You've rerun that Cypress test five times, watching it flip from green to red like a bad coin toss. Here's how we killed the flakiness for good, saving hours and sanity across teams.

Cypress Flaky Tests: Three Code Smells We Eradicated to Reclaim Dev Sanity — The AI Catchup

Key Takeaways

  • Enforce visibility checks before every Cypress click or type to eliminate race conditions.
  • Use cy.intercept() over hard-coded waits for precise, fast network-dependent assertions.
  • Assert animation states explicitly to conquer modals, dropdowns, and dynamic UIs.

Picture this: it’s Friday at 4:55 PM, your Cypress tests are flaking again, and that prod bug two weeks back? Yeah, it started with a shrugged-off red build. For devs grinding daily, Cypress flaky tests aren’t just annoying — they’re the silent thief stealing confidence in your pipeline.

Real people — the engineers, not the C-suite — pay the price. Hours debugging ghosts. Teams ignoring failures because “it’s probably flaky.” Bugs festering in live apps. We’ve been there, across dozens of client projects at BetterQA, wrangling chaos from fifty-plus coders’ war stories.

And here’s the thing.

Flakiness isn’t random. It’s architecture — brittle assumptions about DOM readiness, animations, network timing. Peel back the layers, and three code smells explain 80% of the mess. We fixed ‘em. Flake count plunged. Suite times dropped. Trust returned.

Why Do Cypress Tests Flake on That One Button Click?

Clicking a button sounds simple. cy.get(‘.submit-button’).click(). Boom, done. Except it isn’t.

Cypress snags the element once it’s in the DOM — but DOM existence ain’t readiness. Button’s there, sure. Hidden under a spinner. Disabled mid-validation. Or smothered by a cheeky toast. Race condition city.

Locally? Flawless, your machine’s snappy. CI? Server lag, network hiccups, boom — red.

The fix? Dead simple, brutally effective:

cy.get(‘.submit-button’) .should(‘be.visible’) .and(‘not.be.disabled’) .click()

Those should() assertions? Cypress retries ‘em smartly until truth, or timeout. No more luck-based timing. We made it team law: no bare clicks or types without visibility check. Gripes about wordiness? Vanished when flakes dropped a third.

But wait — this exposes a deeper shift. Cypress sold us on auto-waits over Selenium’s hellish manual sleeps. Yet without discipline, we’re back to square one, chasing phantoms. Our unique twist: treat tests like prod code. Linting enforces it now, via eslint-plugin-cypress. No escapes.

Short para punch: Enforcement matters.

Animations: The Silent Killer in Modals and Menus

Modals fade in. Dropdowns slide. Accordions expand. Pretty UX. Flaky test nightmare.

cy.get(‘.modal’).find(‘.confirm-btn’).click(). Modal’s DOM-ready, button found, click dispatched. But opacity’s 0.3, transitioning. Click hits the backdrop. Or button repositions mid-frame — Cypress targets stale coords.

One healthcare app we fixed? Virtualized table re-renders on scroll. Click row button, table refreshes, button unmounts. Fail.

Real fix, layered:

cy.get(‘.modal’) .should(‘be.visible’) .and(‘have.css’, ‘opacity’, ‘1’) .find(‘.confirm-btn’) .click()

Or class-based: .should(‘not.have.class’, ‘animating’). For that table? Wait for row stability:

cy.get(‘[data-testid=”patient-row”]’) .should(‘have.length.gte’, 10) .first() .find(‘.action-btn’) .should(‘be.visible’) .click()

Ugly? Sure. But 2 AM passes now. Why does this matter? Animations are UX gospel today — React portals, Framer Motion everywhere. Tests must mirror that reality, or they’re worthless.

Historical parallel nobody mentions: this echoes jQuery era plugin soup, where DOM mutations wrecked scripts. Cypress promised escape; we need rituals to earn it.

Hard-Coded Waits: The Laziness Tax

cy.wait(3000). The cardinal sin.

API’s 2s local? Pad to 3s. Staging slows? Bump to 5s, then 8s. Now tests crawl even on fast days, flake under load anyway.

Wastes time. Fails guarantees.

Enter cy.intercept() — the architecture game-changer:

cy.intercept(‘GET’, ‘/api/data’).as(‘loadData’) cy.wait(‘@loadData’) cy.get(‘.data-table’).should(‘have.length.gt’, 0)

Waits precisely for network truth. 50ms response? 50ms wait. 10s lag? Handles it. We swept three projects: flakes down 40%, runtime shaved 12 minutes. Win-win rarity in testing.

Bonus: stubbing unlocks error paths.

cy.intercept(‘POST’, ‘/api/users’, { statusCode: 201 }).as(‘createUser’) // … click, wait(‘@createUser’), assert success.

This isn’t hype — it’s why Cypress laps older tools. But companies spin it as magic; truth is, intercept forces you to model your app’s actual flows. Devs who grok this build antifragile suites.

How to Bulletproof Your Cypress Suite Today

Start small. Audit for bare clicks — add visibility. Hunt animations, assert states. Nuke numeric waits, route ‘em via intercepts.

Skepticism check: BetterQA’s scale helps patterns emerge, but your app’s unique. Still, these cover most sins.

Prediction: As PWAs and SPAs balloon, flaky E2E will bottleneck releases. Teams ignoring this? Watch CI costs explode.

One-line wisdom: Tests aren’t code — they’re contracts.

We’ve seen it transform pipelines. Yours next?


🧬 Related Insights

Frequently Asked Questions

How do I fix flaky Cypress tests quickly?

Prioritize visibility assertions before clicks/types, replace cy.wait(numbers) with cy.intercept(), and assert animation end-states on modals.

What is cy.intercept() and why use it?

It spies/stubs network calls precisely, letting tests wait for real events like API responses instead of arbitrary timers — cuts flakes and speeds runs.

Will these Cypress fixes slow down my tests?

Nope — smart retries and event waits often accelerate suites, as ours dropped 12 minutes total while killing 40% flakes.

Marcus Rivera
Written by

Tech journalist covering AI business and enterprise adoption. 10 years in B2B media.

Frequently asked questions

How do I fix flaky Cypress tests quickly?
Prioritize visibility assertions before clicks/types, replace cy.wait(numbers) with cy.intercept(), and assert animation end-states on modals.
What is cy.intercept() and why use it?
It spies/stubs network calls precisely, letting tests wait for real events like API responses instead of arbitrary timers — cuts flakes and speeds runs.
Will these Cypress fixes slow down my tests?
Nope — smart retries and event waits often accelerate suites, as ours dropped 12 minutes total while killing 40% flakes.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by dev.to

Stay in the loop

The week's most important stories from The AI Catchup, delivered once a week.