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

For a modal rendered in your application’s DOM, use normal Cypress queries: trigger the action that opens it, find the dialog by a stable selector or accessible name, assert it is visible, interact with it, and verify the resulting state. Native browser dialogs such as alert(), confirm(), and prompt() need different handling. Same-origin iframe modals require querying the iframe’s document body; an embedded cross-origin iframe is restricted by the browser’s same-origin policy.

Choose the right approach for the dialog

What you are testing Cypress approach What to watch for
A modal rendered in the application page Query its DOM, assert visibility, interact with controls, and assert the outcome. An element can exist but be covered by an overlay or otherwise not actionable.
Native alert() Listen for window:alert to inspect its message. Cypress automatically accepts alerts; that behavior cannot be changed.
Native confirm() Listen for window:confirm. Cypress accepts it by default; return false to test dismissal.
Native prompt() Stub window.prompt in onBeforeLoad. Install the stub before application code can call the method.
A modal inside a same-origin iframe Read and wrap the iframe’s contentDocument.body, then query within it. Wait for the body to become non-empty before searching.
A modal inside an embedded cross-origin iframe There is no general Cypress DOM-query solution through the browser’s same-origin restriction. cy.origin() handles top-level navigation, not entry into an embedded cross-origin frame.

Access a DOM-rendered modal

A DOM modal is ordinary page content from Cypress’s perspective. Use a stable selector such as a test-specific data-* attribute, or locate the dialog by its accessible role and name. Avoid selectors that depend on incidental layout or styling when a more stable hook is available.

  1. Trigger the control that opens the modal.
  2. Query the dialog and assert that it is visible or open.
  3. Interact with a control inside the dialog.
  4. Assert a meaningful result, such as the dialog closing or the expected page state appearing.
it('opens and closes the settings modal', () => {
  cy.get('[data-cy="open-settings"]').click()

  cy.get('[role="dialog"]')
    .should('be.visible')
    .within(() => {
      cy.get('[data-cy="close-settings"]').click()
    })

  cy.get('[role="dialog"]').should('not.exist')
})

Replace the example selectors with hooks and dialog semantics present in your application. If closing hides the dialog without removing it from the DOM, assert the appropriate hidden or closed state instead of not.exist. The important test is the observable behavior, not a particular implementation choice.

Visibility, overlays, and retry behavior

Cypress checks whether an element is actionable, not merely whether a matching node exists. A target covered by another element—including a modal overlay—can fail an interaction even though a DOM query finds it. Treat a “covered” failure as evidence to inspect the page’s stacking and overlay state: the element may genuinely be unreachable in that state.

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

Assert that the dialog is open before acting on its controls. Cypress retries queries and assertions while waiting for the expected state, so a meaningful assertion is usually better synchronization than a fixed sleep. Do not work around a real overlay problem by forcing a click unless bypassing user-facing actionability is specifically what the test is intended to do.

Handle native alert and confirm dialogs

Native JavaScript dialogs are not DOM modal elements. Cypress automatically accepts alert(), and it automatically accepts confirm() unless a window:confirm handler returns false. Register the handler before the action that opens the dialog.

Inspect an alert message

it('shows the expected alert', () => {
  cy.on('window:alert', (message) => {
    expect(message).to.eq('Saved successfully')
  })

  cy.get('[data-cy="save"]').click()
  cy.get('[data-cy="saved-state"]').should('be.visible')
})

The alert is accepted automatically. The event callback is the place to inspect its message; an assertion after the triggering action can verify the application’s subsequent state.

Accept or dismiss a confirm dialog

To exercise the normal acceptance path, register a handler and inspect the message; returning nothing leaves Cypress’s default acceptance in place. To test dismissal, return false synchronously:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
it('dismisses a confirm dialog', () => {
  cy.on('window:confirm', (message) => {
    expect(message).to.eq('Are you sure?')
    return false
  })

  cy.get('[data-cy="delete"]').click()
  cy.get('[data-cy="deleted-state"]').should('not.exist')
})

In that example, the final assertion is appropriate only if the dismissed branch leaves the deleted-state element absent. Substitute an assertion matching your application’s behavior.

Keep event callbacks synchronous

Cypress event callbacks run outside the normal command queue. Do not put cy.* commands, Cypress assertions that enqueue commands, or cy.task() inside a cy.on() listener. Use synchronous assertions there, or record information with a stub and assert on the stub after the action completes.

Stub a native prompt before the app loads

To control a call to prompt(), install a stub from the onBeforeLoad callback passed to cy.visit(). This ensures the stub exists before application code runs:

it('uses the name entered in a prompt', () => {
  cy.visit('/', {
    onBeforeLoad(win) {
      cy.stub(win, 'prompt').returns('Ada Lovelace')
    },
  })

  cy.get('[data-cy="ask-name"]').click()
  cy.get('[data-cy="greeting"]').should('contain', 'Ada Lovelace')
})

Use the expected effect in your own page for the last assertion. Stubbing supplies a deterministic return value for the application to consume; it is not the same as querying or clicking a DOM-rendered modal.

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

Query a modal inside an iframe

For a same-origin iframe, Cypress documents access through contentDocument.body. Wait for the body to be non-empty, wrap it as a Cypress subject, and then query within the frame:

cy.get('iframe#checkout')
  .its('0.contentDocument.body')
  .should('not.be.empty')
  .then(cy.wrap)
  .find('[role="dialog"]')
  .should('be.visible')
  .contains('button', 'Close')
  .click()

The non-empty assertion matters when frame content renders asynchronously: it allows Cypress to retry before subsequent queries run. The wrapped body becomes the subject for the later find and contains queries.

When the iframe is cross-origin

Browser same-origin restrictions prevent ordinary access to the DOM of an embedded frame from another origin. Cypress’s cy.origin() supports top-level navigation across origins; it does not enter an embedded cross-origin iframe. Cypress documents chromeWebSecurity: false as a possible workaround in Chromium-family environments, with Firefox and WebKit limitations. Treat that as an environment-specific option, not a universal solution for iframe modals.

Use cy.prompt() only when its limits fit

The current Cypress cy.prompt() reference includes natural-language steps such as “dismiss the modal.” It is a convenience layer, not a replacement for understanding the dialog type. The documented constraints include E2E tests only, Chromium-based browsers, and no iframe support, along with other unsupported command areas. When those limits matter, explicit DOM queries, dialog events, or a prompt stub make the behavior under test clearer and more deterministic.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common modal-test failures and fixes

Symptom Likely cause Fix
Click fails because the element is covered An overlay or another element blocks the target. Check whether the dialog or overlay is actually open and whether the target is reachable in that state. Assert the intended open state before interacting.
The confirm dismissal branch is not exercised No handler returned false, so Cypress accepted the confirmation by default. Register window:confirm before the triggering action and return false synchronously.
The alert handler does not affect acceptance Alerts are automatically accepted by Cypress. Use the handler to inspect the message and assert the application’s resulting state separately.
A prompt stub is not used The application invoked prompt() before the stub was installed. Install it in cy.visit()’s onBeforeLoad callback.
A command in a dialog event callback behaves incorrectly The listener runs outside Cypress’s command queue. Keep listener work synchronous; assert on a recorded value or stub after the triggering command.
An iframe query finds no dialog The frame body may not have rendered yet, or the frame may be cross-origin. For a same-origin frame, wait for a non-empty body and wrap it. For an embedded cross-origin frame, account for the browser restriction; cy.origin() does not enter it.
A test passes with a fixed delay but flakes otherwise The test is waiting for elapsed time instead of the state it needs. Assert the dialog’s visibility or another meaningful state so Cypress can retry the query and assertion.

Keep modal tests reliable

  • Register native-dialog handlers before the click or other action that invokes them.
  • Prefer stable data-* selectors or accessible dialog names to brittle layout selectors.
  • Assert the open or visible state before interacting with a modal control.
  • Use state assertions for synchronization instead of arbitrary sleeps.
  • Keep Cypress commands out of cy.on() callbacks.
  • For same-origin iframe content, wait for a non-empty body and wrap it before querying.
  • Investigate covered-element errors as possible real overlay or stacking issues.

Or skip the browser setup

If you need a screenshot of the page around a modal for debugging or documentation, ScreenshotNeo is a separate website screenshot API and MCP server; it does not replace Cypress interaction or modal assertions. One GET request can return an image or PDF. For example, the following cURL request captures the supplied URL as a WebP image; see the ScreenshotNeo API documentation for options and authentication details:

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

ScreenshotNeo removes cookie/consent banners, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to try it with 1,000 screenshots a month and no card.

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.

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