Generated banner for Cypress Flake Fixer
Generated logo for Cypress Flake Fixer

Cypress Flake Fixer

GitHubOfficialFirst committed by qlqlmiDistributed by GitHub

  • Skill
  • Review and audit
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Find and fix flake-prone anti-patterns in an existing Cypress suite, and APIs removed in Cypress 12 to 16. Bundles a zero-dependency scanner for fixed cy.wait() delays, assigned command return values, async/await, force: true, chaining after actions, one-shot expect() inside .then(), conditional testing, and removed APIs such as Cypress.env(), cy.exec(), cy.server()/cy.route(). Use when asked to fix flaky Cypress tests, remove cy.wait, stabilize or harden a Cypress suite, audit Cypress specs, debug "detached from the DOM" errors, or prepare an upgrade to Cypress 15 or 16.
— the description by qlqlmi

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Cypress Flake Fixer is a diagnostic suite designed to audit existing Cypress test projects for common anti-patterns that cause intermittent failures. It provides a zero-dependency scanner that detects problematic code structures like fixed delays, improper command chaining, and conditional testing. The skill offers a structured methodology for replacing these anti-patterns with robust, retry-aware Cypress commands and provides guidance for migrating projects to newer Cypress versions by identifying removed or changed APIs.

What it does

  • Scans test files for flake-prone patterns like cy.wait, force: true, and async/await usage
  • Identifies deprecated or removed Cypress APIs for major version upgrades
  • Provides a systematic workflow to replace brittle waits with network intercepts or state assertions
  • Offers specific guidance on fixing detached-from-the-DOM errors
  • Includes a baseline measurement process to verify test stability improvements
  • Supplies migration paths for sensitive and public environment variables

When to use it

  • When tests pass locally but fail intermittently in CI environments
  • When a test suite contains excessive hard-coded waits or forced interactions
  • When preparing to upgrade a project to Cypress 12 through 16
  • When debugging persistent detached-from-the-DOM or timeout errors
  • When performing a quality audit of existing end-to-end test code

Advantages

  • Improves test reliability without relying on masking failures with retries
  • Reduces test execution time by replacing arbitrary delays with event-driven assertions
  • Provides actionable, rule-based feedback for specific code anti-patterns
  • Facilitates safer major version migrations by flagging breaking API changes
  • Encourages better testing practices by exposing underlying application issues

Usage examples

“Scan my Cypress suite for flaky patterns and deprecated APIs.”
The tool runs a scan across the project directory, reporting findings categorized by rule, such as CY001 for fixed waits or CY007 for removed APIs, and suggests specific refactoring steps.
“How do I fix a test that fails with a detached-from-the-DOM error?”
The skill guides you to identify if the chain is broken by an action that triggers a re-render, suggesting you split the chain or re-query the element after the action.
“I am upgrading to Cypress 16 and need to migrate Cypress.env.”
The skill provides instructions on distinguishing between public values using Cypress.expose and sensitive values using the asynchronous cy.env command.
“Replace a cy.wait(2000) in my checkout test.”
The skill instructs you to identify the network request or state change the wait was covering and replace it with a specific cy.intercept alias or a DOM assertion.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/cypress-flake-fixer/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip cypress-flake-fixer.zip -d .agents/skills

Documentation by qlqlmi

Cypress Flake Fixer

Audit an existing Cypress suite for the code patterns that cause intermittent failures, then fix them without hiding the problem behind retries, longer timeouts, or force: true.

When to Use This Skill

  • A Cypress spec passes locally and fails in CI, or fails once in every few runs

  • The suite contains cy.wait(<number>), { force: true }, or tests that only pass in order

  • Errors mention "detached from the DOM", "timed out retrying", or a subject that "is no longer attached"

  • The project is upgrading Cypress across a major version (12 through 16)

  • A review of Cypress test code is requested

For writing new Cypress tests from scratch, coding standards are enough. This skill is for diagnosing and repairing an existing suite.

Prerequisites

  • Node.js 18 or later to run the scanner (it has no dependencies and does not need Cypress installed)

  • The Cypress version, read from package.json. Fixes differ by major version

Workflow

1. Scan
node ./scripts/scan-cypress.mjs <project-root>
node ./scripts/scan-cypress.mjs <project-root> --json          # machine-readable
node ./scripts/scan-cypress.mjs <project-root> --strict        # add low-confidence rules
node ./scripts/scan-cypress.mjs cypress/e2e/checkout.cy.ts     # one file

The scanner reads the Cypress version from the nearest package.json and grades removed APIs as error when the installed major already rejects them and warn when they break on upgrade. Run --list-rules for the catalog.

Findings are heuristic. Open each location and confirm before editing.

2. Measure before changing anything

When the goal is to fix a specific flaky spec, record how often it fails first. Without a baseline there is no way to tell a fix from luck.

for i in $(seq 1 20); do npx cypress run --spec "cypress/e2e/checkout.cy.ts" --quiet || echo "FAIL $i"; done

A spec that fails 1 run in 10 needs about 30 clean runs after the fix before the fix is believable.

3. Fix in this order
OrderRulesWhy first
1CY002, CY003, CY007 graded errorThe test is wrong or already broken; other fixes build on correct command flow
2CY001, intercept orderingTime-based waits are the largest single source of flake
3CY005, CY006Broken retry-ability causes detached-DOM and one-shot assertion failures
4CY004force: true hides a real overlap, animation, or disabled state
5CY008, CY010, CY012State leaks and order dependence
6CY013, CY101, CY102Selector and assertion hygiene

Before-and-after code for every rule is in references/anti-patterns.md. Replacements for removed APIs are in references/removed-apis.md. Read the section for the rule being fixed; do not load both files upfront.

4. Replace each fixed wait with the condition it was standing in for

Do not delete a cy.wait(n) or swap it for a guess. Work out what the delay covered:

The wait coveredReplace with
A network requestcy.intercept(...).as('x') registered before the trigger, then cy.wait('@x')
A render that follows a request or state changeA .should() on the DOM that results
A debounce, throttle, polling interval, or toast timeoutcy.clock() before cy.visit(), then cy.tick(ms)
A CSS animation or transitionAn assertion on the final state; Cypress already waits for the element to stop moving before acting
Client-side hydration of server-rendered HTMLA signal the app sets when hydration finishes (see Gotchas)
UnknownRun the test in npx cypress open, select the wait in the Command Log, and read which requests and DOM changes happened during it
5. Verify
  1. Run the changed spec alone, then repeat it at least as many times as the baseline run.

  2. Rerun the scanner and compare counts per rule.

  3. Run the full suite once to catch order dependence exposed by the change.

Rules for the fix itself

  • Change test code only. If the test exposes an application bug (double submit, missing loading state, race in the UI), report it instead of working around it.

  • Do not raise defaultCommandTimeout, add retries, or add { force: true } to make a test pass. Each one converts a visible failure into a slower or hidden one.

  • Do not replace cy.wait(2000) with cy.wait(5000), or with .should('exist') on an element that was already present.

  • Keep one rule category per commit so a regression can be bisected.

  • Fix shared custom commands in cypress/support first. A fixed wait inside a command runs in every test that calls it.

Gotchas

  • Register cy.intercept() before the request can fire. For requests made on page load that means before cy.visit(). An intercept registered after the request has left never matches, and cy.wait('@alias') times out intermittently depending on network speed.

  • Each cy.wait('@alias') consumes one request. If the app calls the endpoint twice, the first wait resolves on the first call and the assertion runs against stale data. Wait once per expected call, or assert on the DOM.

  • GraphQL uses one URL for every operation. Alias inside the handler: cy.intercept('POST', '/graphql', (req) => { if (req.body.operationName === 'GetOrders') req.alias = 'getOrders' }).

  • .should('not.exist') passes before the element has rendered. Using it to wait for a spinner to disappear succeeds instantly on a slow page. Assert the positive end state.

  • Server-rendered apps accept clicks before handlers are attached. In Next.js, Nuxt, SvelteKit, and Remix the button is visible and enabled in the server HTML, so Cypress clicks it during hydration and nothing happens. Have the app set a marker when hydration completes (for example a data-hydrated attribute on <body> from a mount effect) and assert on it after cy.visit().

  • A .should() in the middle of a chain pins the subject. Queries after it cannot re-query past the assertion, so a re-render produces a detached-DOM error. Split the chain at the assertion.

  • .then() breaks the retry chain even with no assertion inside. Everything before it runs once.

  • Cypress 16 types with no delay between keys (keystrokeDelay default changed from 10 to 0). Inputs that re-render per keystroke or debounce on input may now drop characters or fire once. Assert the field value after typing instead of restoring the delay.

  • Cypress 16 turned cookie and storage getters into queries. cy.getCookie(), cy.getCookies(), cy.getAllCookies(), cy.getAllLocalStorage(), and cy.getAllSessionStorage() now retry chained .should() assertions, which removes the need for waits around auth cookies.

  • The page is blank after cy.session(). A missing cy.visit() after it looks like a flaky selector failure.

  • testIsolation: false shares state across tests. If the config sets it, every order-dependent failure is expected behavior, not flake. Check the config before diagnosing.

  • Retries hide flake unless configured to report it. To find flaky tests instead of masking them, use the experimental strategy that retries and still fails:

    retries: {
      experimentalStrategy: 'detect-flake-but-always-fail',
      experimentalOptions: { maxRetries: 2, stopIfAnyPassed: true },
      openMode: true,
      runMode: true,
    }
  • CY005 is a judgement call. cy.get(input).clear().type('x') on an input that never re-renders is fine. Split the chain when the element belongs to a list, a form library that remounts fields, or anything driven by async state.

Report format

## Cypress flake audit: <scope>

- Cypress version: <range>, major <n>
- Files scanned: <n>
- Baseline: <spec> failed <f> of <n> runs

| Rule | Before | After | Notes |
|---|---|---|---|
| CY001 fixed-wait | 14 | 0 | 9 replaced by intercept aliases, 5 by assertions |

### Not fixed
- <location>: <reason, for example needs an application change>

### Application issues found
- <behavior the tests exposed>

### Verification
- <spec>: <n> of <n> runs passed after the change

Troubleshooting

IssueSolution
Scanner reports "No Cypress files found"Pass the folder that contains cypress/ or *.cy.* files. Specs outside those conventions can be passed as explicit file paths
A finding is a false positiveAdd // cy-scan-ignore CY0xx on the same or previous line, with the reason
Version shows "not detected"Pass --cypress-major <n>; in a monorepo, run the scanner from the package that depends on Cypress
The test fails only in CI after fixesCompare viewport, browser, and CPU. Run locally with the CI browser and --browser chrome --headless, and throttle CPU to surface timing gaps
cy.wait('@alias') times out after the changeThe intercept is registered after the request, the URL pattern misses a query string (add *), or the request is served from cache and never reaches the network

References

Files

references/anti-patterns.md

Cypress anti-patterns: before and after

One section per scanner rule. Each shows the failing shape, why it flakes, and the replacement.

CY001 fixed-wait

// Before
cy.get('[data-cy=save]').click()
cy.wait(3000)
cy.get('[data-cy=toast]').should('contain', 'Saved')

// After: the wait was covering a request
cy.intercept('PUT', '/api/profile').as('saveProfile')
cy.get('[data-cy=save]').click()
cy.wait('@saveProfile').its('response.statusCode').should('eq', 200)
cy.get('[data-cy=toast]').should('contain', 'Saved')

The delay is either too short on a slow machine (flake) or too long everywhere (slow suite). When the wait covers a timer in the app, fake the clock:

cy.clock()
cy.visit('/search')
cy.get('[data-cy=query]').type('shoes')
cy.tick(300) // the debounce interval
cy.get('[data-cy=result]').should('have.length.greaterThan', 0)

cy.clock() must run before cy.visit() to capture timers the app creates while loading.

CY002 assigned-return-value

// Before
const row = cy.get('[data-cy=row]').first()
row.click()
row.should('have.class', 'selected')

// After
cy.get('[data-cy=row]').first().as('row')
cy.get('@row').click()
cy.get('@row').should('have.class', 'selected')

A command returns a chainable, not an element. Reusing the variable chains new commands onto a subject captured once. An alias re-runs the query each time it is read.

To use a value later in the test, nest or alias the value:

cy.get('[data-cy=order-id]').invoke('text').as('orderId')
cy.get('@orderId').then((orderId) => {
  cy.visit(`/orders/${orderId}`)
})

cy.stub() and cy.spy() are synchronous and may be assigned.

CY003 async-await

// Before
it('loads the user', async () => {
  const user = await cy.request('/api/me')
  expect(user.body.name).to.eq('Ada')
})

// After
it('loads the user', () => {
  cy.request('/api/me').its('body.name').should('eq', 'Ada')
})

Commands are not promises. await resolves at the wrong time or never, and an async test function returns a promise that Cypress races against its own queue. Awaiting a real promise inside cy.then() or cy.wrap(promise) is fine.

CY004 force-true

// Before
cy.get('[data-cy=submit]').click({ force: true })

// After: the button was covered by a cookie banner
cy.get('[data-cy=cookie-accept]').click()
cy.get('[data-cy=cookie-banner]').should('not.exist')
cy.get('[data-cy=submit]').click()

Read the actionability error Cypress printed before force was added. It names the cause:

Error saysUsual causeFix
is being covered by another elementOverlay, sticky header, banner, toastDismiss it, or scroll with scrollIntoView() and assert position
is not visibleCollapsed menu, display: none, zero sizeOpen the parent first
is disabledForm not yet valid, request in flightAssert .should('be.enabled') before acting
is animatingTransition still runningAssert the final state; do not disable the check globally

force: true is legitimate for hidden native inputs behind a styled control, such as a custom file input or checkbox. Leave those and add a comment.

CY005 chain-after-action

// Before
cy.get('[data-cy=todo]').first().click().find('[data-cy=delete]').click()

// After
cy.get('[data-cy=todo]').first().click()
cy.get('[data-cy=todo]').first().find('[data-cy=delete]').click()

An action runs once. If it triggers a re-render, the subject passed down the chain is the old element, and the next command fails with a detached-DOM error or acts on a node that is no longer on screen. A new cy.get() re-queries.

Chaining is fine when the element cannot be replaced: cy.get(input).focus().clear().type('x') on a plain input.

CY006 one-shot-assertion

// Before
cy.get('[data-cy=count]').then(($el) => {
  expect(Number($el.text())).to.eq(3)
})

// After: simple cases
cy.get('[data-cy=count]').should('have.text', '3')

// After: when the value needs processing
cy.get('[data-cy=count]').should(($el) => {
  expect(Number($el.text())).to.eq(3)
})

.then() runs its callback once with whatever the element contained at that instant. .should(callback) re-runs the query and the callback until it stops throwing. Do not call cy commands inside a .should() callback.

.then() is correct after one-shot commands such as cy.request(), cy.task(), cy.wait('@alias'), and cy.env().

CY008 conditional-on-dom

// Before
cy.get('body').then(($body) => {
  if ($body.find('[data-cy=promo-modal]').length) {
    cy.get('[data-cy=promo-close]').click()
  }
})

// After: control the condition
cy.intercept('GET', '/api/promotions', { body: [] })
cy.visit('/')
cy.get('[data-cy=promo-modal]').should('not.exist')

The if runs once, at an arbitrary moment. If the modal renders 50 ms later, the test continues with it open. Remove the uncertainty at its source: stub the response, seed the data, set the cookie or feature flag that decides the branch.

CY009 exclusive-or-debug

Remove .only, cy.pause(), and cy.debug() before committing. A committed .only silently disables every other test in the spec.

CY010 cleanup-in-after-hook

// Before
afterEach(() => { cy.task('db:reset') })

// After
beforeEach(() => { cy.task('db:reset') })

An after hook does not run when the run is cancelled, the browser crashes, or a developer refreshes the runner. The next run then starts from dirty state. Resetting first also leaves the failing state in place for debugging.

CY011 absolute-local-visit

Set baseUrl in cypress.config and call cy.visit('/login'). Without it Cypress loads on a random localhost port and reloads when the first cy.visit() runs, and the same spec cannot run against another environment.

CY012 ui-login-without-session

// cypress/support/commands.js
Cypress.Commands.add('login', (email) => {
  cy.session(['login', email], () => {
    cy.env(['userPassword']).then(({ userPassword }) => {
      cy.request('POST', '/api/login', { email, password: userPassword })
    })
  }, {
    validate() {
      cy.request('/api/me').its('status').should('eq', 200)
    },
  })
})

// in specs
beforeEach(() => {
  cy.login('[email protected]')
  cy.visit('/dashboard')
})
  • Put every input that changes the session in the id array, never the password.

  • The page is blank after cy.session(); call cy.visit().

  • On Cypress older than 15.10, read the password with Cypress.env('userPassword') instead of cy.env().

  • Keep one test that logs in through the UI. Everything else should use the cached session.

CY013 positional-selector and CY101 brittle-selector

// Before
cy.get('ul.results > li:nth-child(2) .btn-primary').click()

// After
cy.get('[data-cy=result]').eq(1).find('[data-cy=open]').click()
// or, when the row is identified by content
cy.contains('[data-cy=result]', 'Invoice 1042').find('[data-cy=open]').click()

Class names change with styling work and positions change with sorting and data. Add data-cy attributes to the application when they are missing; that is a legitimate application change for a test fix.

CY102 negative-assertion-as-wait

// Before
cy.get('[data-cy=save]').click()
cy.get('[data-cy=spinner]').should('not.exist')
cy.get('[data-cy=status]').should('have.text', 'Saved')

// After
cy.get('[data-cy=save]').click()
cy.get('[data-cy=status]').should('have.text', 'Saved')

should('not.exist') passes immediately if the spinner has not rendered yet. The assertion on the final state already waits for the right moment. Keep a negative assertion only after a positive one has proved the page reached the expected state.

CY103 variable-wait

cy.wait(TIMEOUT) with a numeric constant is CY001 behind a name. cy.wait(alias) where the variable holds '@orders' is fine. Check the declaration.

Patterns the scanner cannot see

  • Intercept registered too late. Compare the order of cy.intercept() and the action or cy.visit() that triggers the request.

  • Shared mutable test data. Two specs that edit the same seeded record collide when specs run in parallel. Create a record per test.

  • Order dependence. Run the failing test with .only. If it fails alone, an earlier test was setting state for it.

  • Time and locale. Assertions on formatted dates or "today" fail near midnight and in other time zones. Fix the clock with cy.clock(new Date(2025, 0, 15)).

  • Third-party scripts. Analytics and chat widgets slow page load unpredictably. Block them with the blockHosts config option.

  • Viewport-dependent layout. A menu that collapses below a breakpoint behaves differently in CI when the viewport differs. Set viewportWidth and viewportHeight in the config.

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

First committed by
GitHub avatar of qlqlmiqlqlmi
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/cypress-flake-fixer · commit 797337f
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Contributors
qlqlmi

AutoGineer is not affiliated with or endorsed by qlqlmi or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download cypress-flake-fixer.zip