Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Test the result the user should see, not a timer. Cypress automatically retries a DOM query and its linked assertions, so a test can wait for an element inserted after an Ajax (XHR or fetch) call without cy.wait(1000). When the request itself is part of the behavior you are testing, register cy.intercept() before the action, wait for its alias, and then make a fresh DOM query to verify the rendered result.

Start with a retryable assertion

This is the usual test when the important fact is that asynchronous content eventually appears:

cy.get('[data-testid="results"]')
  .should('be.visible')
  .and('contain', 'Expected result')

cy.get() retries while the linked assertions are failing. The command therefore waits for the element to be inserted and for the expected state or text to be true, subject to Cypress’s normal command timeout. Prefer an assertion that describes a meaningful outcome: expected text, a nonzero count, a status attribute, or the disappearance of a loading indicator. Element existence alone can pass before the application has populated it.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This pattern is documented in Cypress’s retry-ability guidance and its introduction to Cypress. It works whether the page uses XMLHttpRequest, fetch, or a framework that updates the DOM after either one.

Assert the state your user needs

// A list is rendered with at least one result
cy.get('[data-testid="results"] li')
  .should('have.length.greaterThan', 0)

// A loading message goes away and the finished state appears
cy.get('[data-testid="loading"]').should('not.exist')
cy.get('[data-testid="results"]').should('contain', 'Expected result')

// An application-controlled attribute records readiness
cy.get('[data-testid="results"]')
  .should('have.attr', 'data-state', 'ready')

Use stable selectors such as data-testid rather than styling classes that may change. Keep assertions free of side effects; Cypress may execute a callback assertion repeatedly while it is waiting.

Synchronize with a particular Ajax request

If the test must prove that a specific endpoint completed, intercept it before the command that triggers the request:

cy.intercept('GET', '/api/results*').as('getResults')

cy.get('[data-testid="search"]').type('cypress{enter}')
cy.wait('@getResults')

// Query again: this checks what the application rendered.
cy.get('[data-testid="results"]')
  .should('be.visible')
  .and('contain', 'Expected result')

The route matcher must match the actual method and URL. Registering the intercept after typing or clicking is too late because the request may already have been sent. cy.wait('@getResults') synchronizes on the aliased request/response cycle; the new cy.get() is a separate assertion that the application used the response correctly. Cypress explains interception and request inspection in the cy.intercept() API and cy.wait() API.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Inspect the response without confusing it with the UI

cy.intercept('GET', '/api/results*').as('getResults')
cy.get('[data-testid="search"]').type('cypress{enter}')

cy.wait('@getResults').then((interception) => {
  expect(interception.response.statusCode).to.eq(200)
  expect(interception.response.body).to.have.length.greaterThan(0)
})

cy.get('[data-testid="results"]')
  .should('contain', 'Expected result')

Assertions chained directly to cy.wait() run against the yielded interception and do not constitute a retryable DOM query. If you need retry behavior for a yielded property, use a query in the chain, for example cy.wait('@getResults').its('response.statusCode').should('eq', 200). Keep response checks and rendered-HTML checks distinct so a successful network response cannot hide a rendering bug.

Choose real traffic or a stubbed response

Approach Use it when What it establishes Trade-off
Retryable DOM query and assertion The user-visible element or state is the behavior under test The expected UI condition eventually became true It does not identify which request caused the change
cy.intercept() + cy.wait() + fresh DOM query A particular Ajax request is part of the behavior The request completed, then the page displayed the expected result The route must match reliably; a cached request may not reach interception
Real request You want to exercise the actual service and integration path The browser, application, and service work together for the test data Data, availability, and timing can be less deterministic
Stubbed response You need controlled, repeatable data or an error case The UI’s handling of the supplied response It does not test the real service response

Cypress covers the realism-versus-control choice in its network request guide. A stub can be declared inline:

cy.intercept('GET', '/api/results*', {
  statusCode: 200,
  body: [{ id: 1, name: 'Expected result' }],
}).as('getResults')

cy.get('[data-testid="search"]').type('cypress{enter}')
cy.wait('@getResults')
cy.get('[data-testid="results"]')
  .should('contain', 'Expected result')

Use a real request for a contract or integration test, and a stub for deterministic component or UI-state tests. For failures, stub a non-200 response or malformed body and assert the error state rather than waiting for a success element.

Handle rerenders and Cypress retry boundaries

A failed query and its linked assertion retry together. Once that assertion passes, a later command may operate on the subject that passed. Modern applications can then replace that node during a rerender, leaving Cypress with a detached element. Start a new query from the document after a retry boundary:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cy.get('[data-testid="results"]')
  .should('be.visible')

// The app may have replaced the node while finishing its update.
cy.get('[data-testid="results"]')
  .should('contain', 'Expected result')
  .find('button')
  .should('be.enabled')

Do not rely on a previous subject staying attached across an asynchronous update. Cypress does not re-run an action command that has already executed. Avoid chaining a second action onto an element that the application may replace; query it again:

cy.get('[data-testid="save"]').click()
cy.get('[data-testid="edit"]').should('be.visible').click()

For several related checks that must observe one stable retry state, a side-effect-free callback can keep them together:

cy.get('[data-testid="results"]').should(($results) => {
  expect($results).to.be.visible
  expect($results.text()).to.include('Expected result')
  expect($results.find('li')).to.have.length.greaterThan(0)
})

Do not use visibility as a generic “settled” signal

cy.get(selector).should('be.visible').click() checks actionability, but visibility does not necessarily mean that Ajax rendering is complete. A loading transition, replacement node, or delayed data binding can still be in progress. Prefer one of these application signals:

  • the aliased request has completed;
  • a loading element no longer exists or has a finished state;
  • a status or data-* attribute changes to a documented ready value;
  • the final text, count, or control state is present.

These signals describe the condition that matters instead of adding an arbitrary delay. Cypress’s interaction guidance explains actionability checks and why a visible element is not automatically a stable one: Interacting with elements in Cypress.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshoot a test that never sees the rendered element

The alias never resolves

Confirm that cy.intercept() runs before the click or typing action, and that the matcher includes the correct method, path, query string, and origin. Open the browser’s network panel or Cypress’s command log to compare the actual URL. If the browser serves the response from cache, the request may not pass through the network interception layer; disable or account for caching in the test environment, or use the DOM assertion when the request itself is not the subject.

The request passes but the element is empty

A 200 response proves transport, not rendering. Check the response shape expected by the application, then make a fresh DOM query and assert the final text or count. If the stub’s field names differ from production data, the app may render an empty state correctly from Cypress’s perspective.

The element is detached during a click

The application likely replaced it after a state update. Wait for the meaningful state, then query the element again immediately before the action. Split long chains containing multiple actions; Cypress will not replay an action that already changed the page.

The test is flaky only with real requests

Use an intercept to observe the request and assert a deterministic UI condition, or stub the response for a focused UI test. Avoid increasing a fixed sleep: it is slower when the service is fast and still insufficient when the service is slow. Keep a separate integration test for the real service path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The selector finds a hidden or stale copy

Use a selector scoped to the active region, such as cy.get('[data-testid="results-panel"] [data-testid="results"]'), and assert the intended state. If multiple elements are legitimate, assert a count or use .filter(':visible') only when visibility is truly part of the requirement.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Control timeouts only when the application needs it

Cypress’s retry loop uses the command timeout. Set a targeted timeout for a known slow operation rather than adding a global sleep:

cy.get('[data-testid="report"]', { timeout: 15000 })
  .should('contain', 'Ready')

A longer timeout can accommodate a deliberately slow test environment, but it should not compensate for an incorrect route, selector, or application state. Keep the assertion specific so a genuine failure remains diagnosable.

Or skip the browser setup

If your goal is to capture the final HTML-rendered page rather than assert behavior, ScreenshotNeo can run the browser capture for you. Its API accepts a URL and returns PNG, JPEG, WebP, or PDF; options include waiting for a selector, a delay, or network idle, running custom JavaScript, clicking an element, and loading lazy images. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the complete option names and response details in the ScreenshotNeo documentation. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Short checklist

  • Query the element that represents the finished user-visible state.
  • Attach a specific assertion so Cypress can retry the real condition.
  • Register cy.intercept() before the action when a particular request matters.
  • After cy.wait('@alias'), query the DOM afresh.
  • Stub responses when deterministic data matters; use real traffic for integration coverage.
  • Re-query after rerenders and before every action that follows an asynchronous update.
  • Investigate caching, route matching, selectors, and response shape before increasing timeouts.

Frequently Asked Questions

Can I wait for an Ajax element without knowing its endpoint?

Yes. Use a retryable DOM query with an assertion describing the final text, count, attribute, or state. Network interception is optional unless the request itself must be verified.

Does cy.wait(‘@alias’) guarantee that the page rendered the data?

No. It confirms the aliased request cycle, so follow it with a fresh DOM query and an assertion about what the user sees.

Why might a correctly written intercept not fire?

The matcher may be wrong, the intercept may have been registered too late, or the browser may have served the response from cache before it reached the interception layer.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Bottom Line

Use Cypress’s retryable DOM assertions as the default wait. Add a pre-registered cy.intercept() and cy.wait() only when the request is part of the behavior, then always verify the freshly queried rendered result.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.