QA How-To
Cypress handling flaky tests: Examples
Copy Cypress handling flaky tests example patterns for waits, retries, intercept races, stale elements, isolated data, clocks, polling, and CI diagnosis.
25 min read | 3,180 words
TL;DR
Choose the recipe that matches the failure: alias requests before triggers, assert changing DOM state with should, re-query after renders, isolate data, and freeze time. Add a small visible CI retry only after the test remains identical across attempts.
Key Takeaways
- Replace fixed synchronization sleeps with request aliases and retrying visible-state assertions.
- Use should for changing values, keep its callback idempotent, and reserve then for one-time work.
- Register intercepts before application startup or the action that can send the request.
- Re-query framework-rendered elements after state changes instead of reusing stale subjects.
- Make setup unique, retry-safe, parallel-safe, and scoped to records the test owns.
- Use bounded polling only when status polling is the real service contract.
- Keep retries visible and use Cypress.currentRetry only for diagnostic context.
This cypress handling flaky tests example guide turns common intermittent failures into stable, observable tests. Each recipe starts from a recognizable symptom, explains why it changes between runs, and replaces it with current Cypress behavior such as retrying assertions, early intercepts, isolated setup, controlled clocks, and visible retry evidence.
Copy the shape, not the placeholder selectors. A durable fix must synchronize with your application's real contract. The examples use standard Cypress APIs and TypeScript patterns suitable for a 2026 suite.
TL;DR
| Flaky pattern | Stable example |
|---|---|
cy.wait(3000) before checking UI |
Wait on a request alias, then use a retrying assertion |
Read changing text inside .then() |
Assert it inside .should() |
Intercept after cy.visit() |
Register the intercept before the trigger |
| Reuse a shared mutable account | Create run-scoped data in beforeEach |
| Depend on current date | Freeze browser time with cy.clock() |
Click .first() among duplicates |
Scope by stable domain identity |
| Add five retries | Use one visible CI retry while fixing the root cause |
The best test ends at a user-observable outcome and produces useful evidence on its first failed attempt.
1. Cypress Handling Flaky Tests Example for Fixed Waits
A test that sleeps and then checks a result races every slow CI run.
// Flaky
cy.get('[data-cy="refresh-inventory"]').click()
cy.wait(3000)
cy.contains('[data-cy="inventory-status"]', 'Available')
.should('be.visible')
Replace elapsed-time guessing with the request that drives the state:
it('refreshes inventory status', () => {
cy.intercept({
method: 'GET',
pathname: '/api/inventory/sku-42',
}).as('loadInventory')
cy.visit('/products/sku-42')
cy.get('[data-cy="refresh-inventory"]').click()
cy.wait('@loadInventory').then(({ response }) => {
expect(response?.statusCode).to.eq(200)
expect(response?.body).to.include({
sku: 'sku-42',
status: 'available',
})
})
cy.contains('[data-cy="inventory-status"]', 'Available')
.should('be.visible')
})
The route is registered before both visit and click, so it can observe an initial or manual load. If the page sends two calls, use separate aliases or assert a specific match rather than assuming which call cy.wait() consumes.
The network wait explains when the service responded. The visible assertion explains when React, Vue, or another UI layer rendered the result. Keeping both makes failure location clear.
For route matching details, see the Cypress cy.intercept examples.
2. Retry Changing Text with should Instead of then
.then() runs its callback once. If text continues changing after the element first appears, a one-time assertion can fail.
// Flaky
cy.get('[data-cy="sync-status"]').then(($status) => {
expect($status.text().trim()).to.eq('Complete')
})
Attach a retrying assertion:
cy.get('[data-cy="sync-status"]')
.should(($status) => {
expect($status.text().trim()).to.eq('Complete')
})
Cypress reruns the query and callback until the assertion passes or its timeout expires. Keep the callback free of side effects.
For a parsed value:
cy.get('[data-cy="processed-count"]')
.should(($count) => {
const match = $count.text().match(/^Processed (\d+) records$/)
expect(match, 'processed count format').not.to.be.null
expect(Number(match?.[1])).to.eq(25)
})
Do not call .click(), cy.request(), or a data factory inside .should(). A retry could repeat the operation. Perform actions once, then query and assert.
Use .then() after a stable readiness assertion when you need one-time branching or extraction:
cy.get('[data-cy="export-link"]')
.should('have.attr', 'href')
.then(($link) => {
const href = $link.attr('href')
expect(href).to.match(/^\/exports\/\d+$/)
})
This distinction is one of the highest-value Cypress flake fixes because it uses the framework's intended retry model without increasing global timeouts.
3. Register Intercepts Before Application Startup
A page can request data immediately during startup. Registering afterward creates a race.
// Flaky
cy.visit('/dashboard')
cy.intercept('GET', '/api/dashboard').as('dashboard')
cy.wait('@dashboard')
Stable version:
it('loads the dashboard summary', () => {
cy.intercept('GET', '/api/dashboard').as('loadDashboard')
cy.visit('/dashboard')
cy.wait('@loadDashboard')
.its('response.statusCode')
.should('eq', 200)
cy.contains('h1', 'Dashboard').should('be.visible')
cy.get('[data-cy="summary-card"]').should('have.length.at.least', 1)
})
If several GraphQL operations share one endpoint, alias the request by operation:
cy.intercept('POST', '/graphql', (req) => {
if (req.body.operationName === 'DashboardSummary') {
req.alias = 'gqlDashboardSummary'
}
})
cy.visit('/dashboard')
cy.wait('@gqlDashboardSummary').then(({ response }) => {
expect(response?.body.errors).to.be.undefined
})
Do not fix a timeout with a catch-all route. A broad route may capture a font, telemetry call, or unrelated API and let the test continue before dashboard data arrives.
Browser cache and service workers can prevent expected network traffic. When an early intercept still records zero matches, inspect developer tools and caching behavior before changing the test.
4. Re-Query After a Framework Re-Render
Modern frameworks often replace DOM nodes. Saving a jQuery element and using it after a state change can operate on a detached subject.
// Fragile
cy.get('[data-cy="quantity"]').then(($quantity) => {
cy.get('[data-cy="increment"]').click()
expect($quantity).to.have.value('2')
})
Query the current DOM after the action:
cy.get('[data-cy="quantity"]').should('have.value', '1')
cy.get('[data-cy="increment"]').click()
cy.get('[data-cy="quantity"]').should('have.value', '2')
For a row that can move after sorting, locate by domain text each time:
const projectRow = () => {
return cy.contains('[data-cy="project-row"]', 'Checkout reliability')
}
projectRow().within(() => {
cy.contains('button', 'Pin').click()
})
projectRow()
.should('have.attr', 'data-pinned', 'true')
.and('be.visible')
The helper executes a new query on every call. It does not cache an element.
Avoid positional selectors such as .eq(2) after reordering unless position is the requirement. Stable identity can be accessible text, a unique label, or a test attribute backed by a domain ID.
A detached-element error may also expose an application bug if a control is replaced during interaction. Confirm the product behavior instead of automatically adding force.
5. Wait for Loading and Actionability States
A spinner disappearing is useful only if it has a defined lifecycle. Pair it with the final state.
it('saves account preferences', () => {
cy.intercept('PUT', '/api/preferences').as('savePreferences')
cy.visit('/settings')
cy.get('[data-cy="weekly-summary"]').check()
cy.contains('button', 'Save changes')
.should('be.enabled')
.click()
cy.get('[data-cy="saving-indicator"]').should('be.visible')
cy.wait('@savePreferences')
.its('response.statusCode')
.should('eq', 200)
cy.get('[data-cy="saving-indicator"]').should('not.exist')
cy.contains('[role="status"]', 'Changes saved').should('be.visible')
})
Cypress actionability already waits for a button to become visible, enabled, uncovered, and stable enough to click. The explicit enabled assertion communicates intent and helps diagnose a permanently disabled control.
Do not assert that a spinner appears if a fast response can legitimately complete before the browser paints it. In that case, control the response for a dedicated loading-state test:
cy.intercept('GET', '/api/reports', {
statusCode: 200,
body: { items: [] },
delay: 500,
}).as('loadReports')
The delay is a deterministic test fixture for the loading UI, not a wait used to synchronize the test. Keep integrated happy paths free from artificial latency.
6. Make Shared Data Unique and Repeatable
A hardcoded record collides across retries and parallel jobs.
// Flaky in parallel
cy.request('POST', '/api/projects', {
name: 'Automation Project',
})
Create a run-scoped identifier supplied by CI, with a safe local fallback:
const createProjectName = () => {
const runId = Cypress.expose('runId') ?? 'local'
return `qa-${runId}-${Cypress._.uniqueId('project-')}`
}
beforeEach(() => {
const name = createProjectName()
cy.request('POST', '/api/test-support/projects', {
name,
status: 'active',
}).then(({ body }) => {
cy.wrap({ id: body.id, name }).as('project')
})
})
it('archives its own project', function () {
cy.visit(`/projects/${this.project.id}`)
cy.contains('button', 'Archive').click()
cy.contains('Project archived').should('be.visible')
})
runId is public coordination data, so Cypress.expose() is appropriate. Configure it under expose or with --expose. Do not put a secret there.
Use a server-generated ID as the primary cleanup key. A process-local unique counter alone is not globally unique, which is why the CI run ID matters.
If retries repeat beforeEach, new setup may create another record. Either make the factory idempotent for a stable test key or clean every created record through an isolated tenant. Cleanup must never delete another worker's data.
7. Control Date and Timer Flake
A relative-date test can change meaning while it runs. Freeze browser time before visiting.
it('shows a trial as expired', () => {
const now = new Date('2026-07-13T12:00:00Z')
cy.clock(now.getTime())
cy.intercept('GET', '/api/subscription', {
body: {
plan: 'trial',
expiresAt: '2026-07-13T11:59:59Z',
},
}).as('loadSubscription')
cy.visit('/billing')
cy.wait('@loadSubscription')
cy.contains('Trial expired').should('be.visible')
})
For an in-browser countdown:
it('enables resend after thirty seconds', () => {
cy.clock(new Date('2026-07-13T12:00:00Z').getTime())
cy.visit('/verify-email')
cy.contains('button', 'Resend email').should('be.disabled')
cy.tick(30000)
cy.contains('button', 'Resend email').should('be.enabled')
})
Call cy.clock() before application startup when startup schedules the timer. Remember that it controls browser timers, not a remote service clock. Stub server timestamps or use an approved service time control for cross-boundary scenarios.
Use ISO timestamps with explicit offsets. A string such as 2026-07-13 may be interpreted differently across libraries and timezones. Keep locale-format assertions separate from business expiration logic.
8. Stabilize Search and Debounce Tests
Typing into a debounced search can race the request. Control the browser clock when the debounce behavior itself matters.
it('searches after the debounce interval', () => {
cy.clock()
cy.intercept({
method: 'GET',
pathname: '/api/customers',
query: { q: 'avery' },
}).as('searchCustomers')
cy.visit('/customers')
cy.get('input[aria-label="Search customers"]').type('avery')
cy.tick(300)
cy.wait('@searchCustomers')
cy.contains('[data-cy="customer-row"]', 'Avery Chen')
.should('be.visible')
})
The 300 milliseconds must match an owned documented debounce setting. If the number is an implementation detail likely to change, use an application readiness signal or test the exact debounce in a component test.
Do not type and immediately assert "no request" before the debounce interval. The negative assertion can pass before a late request occurs. A component test with a controlled clock can prove no early callback and one callback after the threshold.
For an integrated E2E search that does not care about debounce, wait on the search alias and visible result without asserting the precise delay. Separate behavior from implementation timing.
9. Fix Ambiguous Selector Flake
Text may occur in navigation, cards, and dialogs. Cypress can match the wrong one when layout changes.
// Ambiguous
cy.contains('Delete').click()
Scope to the record and then the confirmation dialog:
cy.contains('[data-cy="project-row"]', 'Legacy checkout')
.within(() => {
cy.contains('button', 'Delete').click()
})
cy.get('[role="dialog"]')
.should('contain.text', 'Delete Legacy checkout?')
.within(() => {
cy.contains('button', 'Delete project').click()
})
cy.contains('[data-cy="project-row"]', 'Legacy checkout')
.should('not.exist')
The final assertion re-queries the collection. Avoid keeping the deleted row as a subject and asserting against a detached node.
When visible text changes by locale or content design, use a stable attribute with meaningful scope:
cy.get('[data-cy="project-row"][data-project-id="project-42"]')
.find('[data-cy="delete-project"]')
.click()
Do not use .first() to silence the multiple-match warning. If the first item truly matters, assert the ordering contract before selecting it.
Review the Cypress best practices guide for selector and isolation conventions that scale across teams.
10. Poll a Background Job with a Bounded Helper
Some systems expose a job-status API without pushing an event to the page. Polling is legitimate when it is the product contract, but make the helper bounded and diagnostic.
type Job = {
id: string
status: 'queued' | 'running' | 'ready' | 'failed'
}
const waitForJob = (
id: string,
attemptsLeft = 5,
): Cypress.Chainable<Cypress.Response<Job>> => {
return cy.request<Job>(`/api/jobs/${id}`).then((response) => {
if (response.body.status === 'ready') {
return cy.wrap(response, { log: false })
}
if (response.body.status === 'failed') {
throw new Error(`Job ${id} failed`)
}
if (attemptsLeft === 1) {
throw new Error(`Job ${id} did not become ready`)
}
return cy.wait(1000, { log: false }).then(() => {
return waitForJob(id, attemptsLeft - 1)
})
})
}
cy.request<Job>('POST', '/api/jobs', { reportId: 42 })
.then(({ body }) => waitForJob(body.id))
.its('body.status')
.should('eq', 'ready')
Here the one-second interval is part of a bounded polling algorithm, not a blind synchronization sleep. The helper stops on ready, fails immediately on failed, and produces a clear timeout after five checks.
Tune attempts and interval to the test service contract. For a long-running workflow, prefer a test hook that accelerates processing or a lower-level integration test. Do not turn a browser job into minutes of silent polling.
11. Configure One Visible CI Retry
A small retry allowance can reveal inconsistent outcomes while the team fixes causes.
import { defineConfig } from 'cypress'
export default defineConfig({
retries: {
runMode: 1,
openMode: 0,
},
video: true,
screenshotOnRunFailure: true,
})
One run-mode retry means at most two attempts. The test's beforeEach and afterEach hooks run for each attempt. before and after hook failures do not trigger a test retry.
Use the current retry index only for diagnostics:
it('loads account activity', { retries: 1 }, () => {
cy.log(`retry index: ${Cypress.currentRetry}`)
cy.visit('/account/activity')
cy.get('[data-cy="activity-row"]').should('have.length.at.least', 1)
})
Do not skip setup, change endpoints, extend assertions, or accept a weaker result when Cypress.currentRetry > 0. The same test should execute each time.
Preserve screenshots and video from failed attempts. Cypress adds attempt suffixes to retry artifacts. Your reporter or CI dashboard should flag fail-then-pass as flaky rather than showing an unqualified green result.
The Cypress test retries guide can complement this example with project-wide policy choices.
12. Use an Experimental Always-Fail Flake Policy
Some teams want any inconsistent outcome to fail the build. Current Cypress experimental retry strategies can detect flake and control final status. One strategy is detect-flake-but-always-fail.
Because experimental configuration can change, pin Cypress and verify the exact supported schema for your installed release before adoption. The stable alternative is to use numeric retries and make the result processor fail on recorded flaky outcomes.
A threshold-based experimental configuration is explicit:
import { defineConfig } from 'cypress'
export default defineConfig({
retries: {
experimentalStrategy: 'detect-flake-and-pass-on-threshold',
experimentalOptions: {
maxRetries: 2,
passesRequired: 2,
},
openMode: true,
runMode: true,
},
})
This requires two passing attempts within the permitted retry budget after failure. With experimental retry strategies, global openMode and runMode are Booleans, not the numeric counts used by stable retries.
Pilot the policy on a small CI lane. Verify reporter output, artifact retention, run cost, and team triage capacity. A stricter status policy exposes debt but does not fix selectors, data, product races, or infrastructure.
13. Diagnose CI-Only Failures with a Reproduction Matrix
Capture the dimensions that differ between local and CI.
| Dimension | Local value | CI value | Reproduction action |
|---|---|---|---|
| Browser and version | Interactive Chrome | Headless Electron or Chrome | Run the same browser locally |
| Viewport | Developer window | Configured CI size | Use the CI viewport |
| Data | Personal test tenant | Shared tenant | Use an isolated CI-like tenant |
| CPU and memory | Workstation | Limited container | Apply representative resource limits |
| Locale and timezone | User default | Runner default | Set explicit values |
| Parallelism | One spec | Several shards | Run the conflicting specs together |
| Feature flags | Local defaults | Pipeline overrides | Inspect resolved configuration |
Run the single spec repeatedly only after matching these inputs. A local passing loop against different services does not reproduce the failure.
Compare the first failed attempt across runs. A recurring detached element suggests re-rendering, repeated 409 suggests data collision, alias timeout suggests route timing or matching, and browser crash suggests resource pressure.
Avoid turning off video, console capture, or server diagnostics before understanding the issue. Artifacts cost storage, but they often provide the only evidence for low-frequency failures. Sanitize secrets and personal data in all retained output.
14. Cypress Handling Flaky Tests Example Triage Checklist
When a test fails intermittently, work through this order:
- Read the first failed command and assertion.
- Confirm whether the application state was correct, delayed, or wrong.
- Inspect owned network calls and register missing aliases before triggers.
- Check selector uniqueness and whether the node re-rendered.
- Verify test data ownership, retry repeatability, and parallel cleanup.
- Freeze clock, locale, and randomness if they influence the scenario.
- Compare browser, flags, viewport, and resources with CI.
- Decide whether the defect belongs to test, product, service, or infrastructure.
- Implement one root-cause fix and retain a focused regression assertion.
- Run a targeted repetition under the same conditions and monitor history.
Do not change three timeouts, a selector, and retry count at once. You may make the symptom disappear without learning which change mattered.
Document the failure signature and fix in the issue or pull request. Future reviewers can recognize recurrence and avoid reintroducing the unstable pattern.
A team that uses this checklist consistently turns flake from folklore into engineering work. The goal is not a perfect dashboard for one day. It is a suite whose failures reliably describe product and environment risk.
15. Separate Product Races from Test Races
A flaky assertion does not automatically mean the test is wrong. Ask whether the application ever reached an invalid state that a customer could observe. Pair the UI evidence with an owned service boundary to distinguish late rendering from incorrect behavior.
Suppose a Save request returns 200 but the page sometimes restores the old value. Capture both facts:
it('keeps the saved display name after refresh', () => {
cy.intercept('PUT', '/api/profile').as('saveProfile')
cy.intercept('GET', '/api/profile').as('loadProfile')
cy.visit('/profile')
cy.wait('@loadProfile')
cy.get('input[name="displayName"]')
.clear()
.type('Avery Chen')
cy.contains('button', 'Save').click()
cy.wait('@saveProfile').then(({ request, response }) => {
expect(request.body.displayName).to.eq('Avery Chen')
expect(response?.statusCode).to.eq(200)
})
cy.reload()
cy.wait('@loadProfile').then(({ response }) => {
expect(response?.body.displayName).to.eq('Avery Chen')
})
cy.get('input[name="displayName"]')
.should('have.value', 'Avery Chen')
})
If the PUT contains the right value but the following GET returns the old value, the likely defect is service consistency or caching. If the GET is correct but the input shows old text, investigate client state. If the initial request is wrong, inspect the form and test interaction. The same failed UI assertion now has boundary evidence.
Do not stub every request while investigating a suspected product race. A fully controlled response can make the symptom disappear by removing the faulty integration. Reproduce against the real owned services first, then add a deterministic stubbed regression for a frontend-only state if appropriate.
Also check whether the test performed an impossible user sequence. Force-clicking a disabled control, bypassing required navigation, or mutating storage directly can create a race customers cannot reach. Repair the test when its interaction is invalid. File an application defect when the observed sequence is legitimate and the system loses, duplicates, or misorders user work.
16. Prove and Monitor the Flake Fix
A single passing run after an edit is weak evidence. Define what the fix changed, reproduce the original stress condition, and monitor the failure signature after merge. Keep the verification proportional to business risk.
For a local repetition, use the Cypress CLI around the focused spec:
for run in 1 2 3 4 5; do
npx cypress run \
--browser chrome \
--spec cypress/e2e/profile.cy.ts || exit 1
done
Five runs are illustrative, not a guarantee of perfect stability. Use the same browser, viewport, target environment, feature flags, timezone, and parallel neighbors that exposed the problem. If the flake required shared data collision, an isolated single-spec loop will not test the fix. Run the conflicting specs in the same parallel arrangement.
Compare evidence before and after: alias match counts, record identifiers, first-failure command, request status, duration distribution, and browser errors. Avoid celebrating only a lower failure count if retry consumption or runtime increased sharply.
After merge, track the exact test and signature across scheduled and pull request runs. A renamed test can fragment history, so keep stable titles unless the behavior changed. Close the issue with the cause, code change, reproduction profile, and observed follow-up period.
Do not remove retry visibility immediately if doing so destroys comparison data. First confirm that fail-then-pass events stop, then return any temporary per-test retry override to the project policy. Remove temporary diagnostic logging that exposes noisy or sensitive payloads, but retain meaningful aliases and assertions.
A reliable fix leaves the test simpler or more explicit. If it adds several force options, global timeouts, conditional retry logic, and broad intercepts, the suite may be quieter while confidence is worse. Review the final diff as a testing design change, not only as a green-run change.
Evidence That a Fix Is Durable
A durable fix should remain understandable to someone who did not investigate the incident. Keep the final test focused on the real synchronization or isolation rule. Delete temporary catch-all routes, console dumps, repeated screenshots, and local-only switches. Retain descriptive aliases, stable selectors, controlled data, and the assertion that would fail if the defect returned.
Review the application change and test change together when the root cause crosses both. For example, the product might add an explicit ready status while the test replaces a sleep with an assertion on that status. Neither side should be presented as a mysterious timing adjustment. The pull request should explain the previous race and the new contract.
Watch neighboring tests for the same pattern. One late intercept or shared user often indicates a copied helper or setup convention. Search for the pattern and create a focused follow-up rather than expanding the current change without review. A small shared utility can help when it exposes intent, but a large wrapper can hide which request or state a scenario needs.
Track recurrence by failure signature, not just test title. A test can have several unrelated flakes, and the same data collision can affect several tests. Tags such as route alias timeout, duplicate record, detached node, or browser crash make history actionable.
Finally, distinguish confidence from certainty. Repetition and a clean monitoring period reduce the chance that the known trigger remains, but they cannot prove a test will never fail. The stronger claim is concrete: the test no longer guesses time, owns its data, observes the correct boundary, and has not reproduced the original signature under the relevant execution profile.
Interview Questions and Answers
Q: Give a Cypress handling flaky tests example involving a fixed wait.
Register the request before the trigger, wait on its alias, and then use a retrying UI assertion. This replaces guessed time with two observable boundaries.
Q: When should an assertion use should instead of then?
Use should when the queried value may change and the assertion is idempotent. Use then for one-time work after the state is already stable.
Q: How do you fix detached element failures?
Do not cache the old jQuery element across a re-render. Perform the action, then query the element again by stable identity.
Q: How do you make a data factory retry-safe?
Use a stable run and test key or record every created ID, make setup repeatable, and clean only owned records. Account for beforeEach running again on retry.
Q: Is a polling wait always an anti-pattern?
No. Bounded polling is valid when the service contract exposes status that way. It must stop on success or failure and have a clear maximum.
Q: How should Cypress.currentRetry be used?
Use it for diagnostic context only. Changing business behavior, setup, or acceptance criteria on later attempts hides flake.
Q: What is the first artifact to inspect?
Inspect the initial failed attempt, including command, screenshot, network, console, and environment. A successful retry can erase the triggering state.
Common Mistakes
- Copying a timeout increase without identifying the delayed condition.
- Putting actions or API calls inside a retrying
.should()callback. - Registering a route after page startup already requested it.
- Reusing a detached jQuery element after React or Vue renders again.
- Using a process-local counter as a globally unique parallel ID.
- Freezing browser time after the application schedules timers.
- Asserting a transient spinner must appear on every fast integrated run.
- Testing exact debounce duration in a broad E2E scenario.
- Writing unbounded recursive polling.
- Changing assertions when
Cypress.currentRetryis greater than zero. - Treating an experimental retry strategy as a root-cause fix.
- Reproducing a CI failure with different browser, data, flags, or services.
- Editing several synchronization mechanisms at once.
Conclusion
A strong cypress handling flaky tests example replaces an invisible race with an explicit contract. Use retrying assertions for changing DOM state, early intercepts for requests, fresh queries after re-rendering, isolated records for parallel runs, and controlled time for date-sensitive behavior.
Begin with the example closest to your failure signature. Adapt it to the application's real event, preserve first-attempt evidence, and verify the fix under the same CI dimensions that exposed the flake.
Interview Questions and Answers
Show how you replace a fixed wait in Cypress.
I register cy.intercept before the action, trigger the behavior, wait on the named alias, and assert the retrying user-visible result. The request and render boundaries make the failure diagnostic.
When do you use should callbacks?
I use them for idempotent assertions on values that may change, such as parsed text or counts. I never put clicks, requests, or data creation inside because Cypress can rerun the callback.
How do you handle DOM re-rendering?
I avoid caching a subject across the state change. After the action I query again by accessible or stable domain identity and assert the new state.
How do you design retry-safe setup?
I assume beforeEach can run more than once, use run-scoped identity, make creation idempotent or track every created ID, and clean only owned records. Shared mutable accounts are avoided.
When is a fixed cy.wait acceptable?
It is acceptable when elapsed time or a polling interval is the behavior being tested, especially with cy.clock or a bounded helper. It is not a general synchronization mechanism.
How would you introduce strict flake detection?
I first ensure flaky outcomes and artifacts are visible, reduce existing debt, then pilot Cypress experimental strategies or a result policy in one CI lane. I pin versions and verify reporter behavior before broad adoption.
What do you compare for a CI-only flake?
I compare browser, viewport, environment, flags, data ownership, locale, timezone, resources, and shard neighbors. I use the first failed attempt to identify a recurring signature.
Frequently Asked Questions
What is the most common Cypress flaky test fix?
Replace a fixed wait with an observable condition. Register a request alias before the trigger and follow it with a retrying assertion on the final UI state.
Why does a Cypress assertion in then fail intermittently?
The then callback runs once after the element resolves, even if its text is still changing. Put an idempotent assertion in should so Cypress can re-query and retry.
How do I fix Cypress detached element errors?
Do not store a jQuery element across a framework re-render. Perform the action, then issue a fresh query using stable domain identity.
How do I prevent Cypress data collisions in parallel CI?
Use a CI run identifier plus a test-specific value, rely on server-generated IDs, isolate mutable accounts, and delete only records owned by the test.
Can I use cy.wait in a polling helper?
Yes when the interval is part of a bounded status-polling contract. Limit attempts, stop immediately on terminal failure, and return a clear error when the maximum is reached.
How do I configure one Cypress retry only in CI mode?
Set retries to { runMode: 1, openMode: 0 } in cypress.config.ts. Continue reporting any fail-then-pass outcome as flaky.
How do I reproduce a Cypress test that fails only in CI?
Match the CI browser, viewport, target environment, feature flags, data, timezone, resources, and parallel spec combination. Then compare first-attempt artifacts by failure signature.