Cypress Flake Fixer
GitHubOfficialFirst committed by qlqlmiDistributed by GitHub
- Skill
- Review and audit
- Software development
- MIT
- Low risk
- Has scripts
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
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/github/cypress-flake-fixer/downloadUnzip 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 orderErrors 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 fileThe 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"; doneA 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
| Order | Rules | Why first |
|---|---|---|
| 1 | CY002, CY003, CY007 graded error | The test is wrong or already broken; other fixes build on correct command flow |
| 2 | CY001, intercept ordering | Time-based waits are the largest single source of flake |
| 3 | CY005, CY006 | Broken retry-ability causes detached-DOM and one-shot assertion failures |
| 4 | CY004 | force: true hides a real overlap, animation, or disabled state |
| 5 | CY008, CY010, CY012 | State leaks and order dependence |
| 6 | CY013, CY101, CY102 | Selector 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 covered | Replace with |
|---|---|
| A network request | cy.intercept(...).as('x') registered before the trigger, then cy.wait('@x') |
| A render that follows a request or state change | A .should() on the DOM that results |
| A debounce, throttle, polling interval, or toast timeout | cy.clock() before cy.visit(), then cy.tick(ms) |
| A CSS animation or transition | An assertion on the final state; Cypress already waits for the element to stop moving before acting |
| Client-side hydration of server-rendered HTML | A signal the app sets when hydration finishes (see Gotchas) |
| Unknown | Run 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
Run the changed spec alone, then repeat it at least as many times as the baseline run.
Rerun the scanner and compare counts per rule.
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, addretries, 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)withcy.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/supportfirst. 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 beforecy.visit(). An intercept registered after the request has left never matches, andcy.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-hydratedattribute on<body>from a mount effect) and assert on it aftercy.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 (
keystrokeDelaydefault 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(), andcy.getAllSessionStorage()now retry chained.should()assertions, which removes the need for waits around auth cookies.The page is blank after
cy.session(). A missingcy.visit()after it looks like a flaky selector failure.testIsolation: falseshares 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 changeTroubleshooting
| Issue | Solution |
|---|---|
| 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 positive | Add // 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 fixes | Compare 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 change | The 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
references/anti-patterns.md: before-and-after code for every scanner rule
references/removed-apis.md: removed APIs by Cypress major, with migration code for
Cypress.env()andcy.exec()
Files
- ATTRIBUTION.mdAdded by AutoGineer867 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md9.9 KiB
- references/anti-patterns.md8.6 KiB
- references/removed-apis.md7.1 KiB
- scripts/scan-cypress.mjsScript22.7 KiB
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 says | Usual cause | Fix |
|---|---|---|
| is being covered by another element | Overlay, sticky header, banner, toast | Dismiss it, or scroll with scrollIntoView() and assert position |
| is not visible | Collapsed menu, display: none, zero size | Open the parent first |
| is disabled | Form not yet valid, request in flight | Assert .should('be.enabled') before acting |
| is animating | Transition still running | Assert 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
idarray, never the password.The page is blank after
cy.session(); callcy.visit().On Cypress older than 15.10, read the password with
Cypress.env('userPassword')instead ofcy.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 orcy.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
blockHostsconfig option.Viewport-dependent layout. A menu that collapses below a breakpoint behaves differently in CI when the viewport differs. Set
viewportWidthandviewportHeightin 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
qlqlmi
- 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