Handling Alerts, Confirms, and Popups

7 min read

When the page interrupts the user, your tests have to interrupt the same way. Cypress draws a clean line between two kinds of interruption: native browser dialogs (window.alert, window.confirm, window.prompt) and HTML modal dialogs (cookie banners, custom confirmation modals, anything built with <div> and CSS). The native ones are handled by Cypress event listeners; the HTML ones are handled with regular cy.get. Knowing which is which is the entire lesson.

Native browser dialogs are auto-accepted

By default, Cypress automatically accepts every window.alert, clicks OK on every window.confirm, and stubs window.prompt to return undefined. Your test never sees a blocking dialog. This is intentional: the dialogs are blocking by design, and a test paused waiting for human input would deadlock.

it("clicks Add to cart even though the page calls window.alert", () => {
  cy.visit("/products");
  cy.get("[data-testid='add-to-cart']").click();
  // Page calls alert("Item added!"). Cypress auto-dismisses it.
  cy.get("[data-testid='cart-count']").should("have.text", "1");
});

You don't need any setup for the auto-accept behaviour. It's the default. If a test "doesn't work" because of an alert, the alert isn't actually blocking — you just don't see it during the run.

Asserting on alert text

When you want to know an alert fired and what it said, hook the window:alert event and inspect the message:

it("triggers the correct alert text on add-to-cart", () => {
  cy.on("window:alert", (text: string) => {
    expect(text).to.equal("Item added to cart!");
  });
 
  cy.visit("/products");
  cy.get("[data-testid='add-to-cart']").click();
});

cy.on("window:alert", ...) registers a one-off listener for this test. The handler receives the alert's text. Use expect (not should) inside the callback — it runs synchronously when the alert fires.

The same pattern works for window:confirm. The difference: returning false from the handler is how you simulate clicking Cancel.

Confirm — clicking OK or Cancel

By default Cypress clicks OK on window.confirm. To make it click Cancel, return false from a window:confirm listener:

it("cancels the delete confirmation", () => {
  cy.on("window:confirm", () => false);
 
  cy.visit("/items/123");
  cy.get("[data-testid='delete-item']").click();
  // Confirm appears, listener returns false → Cancel clicked.
  cy.get("[data-testid='item']").should("be.visible"); // item still there
});

To assert on the confirm text and control the choice:

cy.on("window:confirm", (text: string) => {
  expect(text).to.equal("Are you sure you want to delete this item?");
  return true;   // click OK
});

return true (or omitting the return) clicks OK. return false clicks Cancel. The text assertion runs every time the dialog fires.

Prompt — providing input via stub

window.prompt doesn't ship with a default Cypress event hook. Instead, you stub the window.prompt function before the code that triggers it runs:

it("renames an item via the prompt", () => {
  cy.visit("/items/123");
 
  cy.window().then((win) => {
    cy.stub(win, "prompt").returns("Renamed item");
  });
 
  cy.get("[data-testid='rename-btn']").click();
  // Code calls window.prompt(...) — stub returns "Renamed item".
  cy.get("[data-testid='item-name']").should("have.text", "Renamed item");
});

cy.stub(win, "prompt") replaces the real window.prompt for the duration of the test. The .returns(...) call sets what the stub will give back to the application. Restore behaviour is automatic at the end of the test — no teardown needed.

Browser dialogs vs HTML modals — pick the right tool

Native browser dialogs and HTML modals look similar to a user but are completely different to a test:

Native browser dialogs vs HTML modal dialogs

Native browser dialogs

  • alert(), confirm(), prompt() — JavaScript runtime APIs

  • Rendered by the browser, not the DOM

  • Block the JavaScript event loop until dismissed

  • Handled with cy.on('window:alert' / 'window:confirm') and cy.stub on prompt

HTML modal dialogs

  • Custom <div>, <dialog>, or React/Vue components

  • Live in the DOM like any other element

  • Don't block — they overlay with z-index and CSS

  • Handled with cy.get / cy.contains / cy.click — same as any other element

The way to tell them apart is to inspect what the trigger calls. If the source code says if (window.confirm("Delete?")) { ... }, it's a native confirm — use cy.on("window:confirm", ...). If the click sets a state variable that renders <div role="dialog">, it's an HTML modal — query it with cy.get.

Cookie banners are almost always HTML dialogs. They overlay the page with position: fixed, but they're regular DOM. Click through them like anything else:

beforeEach(() => {
  cy.visit("/");
  cy.get("[data-testid='cookie-accept']").click();
});

If the banner only sometimes appears (returning users won't see it after the consent cookie is set), guard the click so a missing banner doesn't fail the test:

Cypress.Commands.add("dismissCookieBanner", () => {
  cy.get("body").then(($body) => {
    if ($body.find("[data-testid='cookie-accept']").length > 0) {
      cy.get("[data-testid='cookie-accept']").click();
    }
  });
});

$body.find(...).length is a synchronous jQuery check — no retry, no failure if the banner is absent. The conditional pattern is one of the few legitimate uses of if/then logic in a Cypress test. Reach for it when an element is genuinely optional.

A cleaner alternative for cookie banners: set the consent cookie before the page loads, so the banner never appears at all:

beforeEach(() => {
  cy.setCookie("cookie-consent", "accepted");
  cy.visit("/");
});

Most cookie-banner libraries read a known cookie or localStorage key. Pre-seeding it skips the banner entirely and removes a click from every test.

A delete-flow test that exercises everything

Bringing the patterns together — a typed test that hits a native confirm, an HTML modal, and a final native alert:

describe("Delete item flow", () => {
  beforeEach(() => {
    cy.visit("/items/42");
  });
 
  it("cancels the native confirm and keeps the item", () => {
    cy.on("window:confirm", (text) => {
      expect(text).to.contain("Are you sure");
      return false;          // Cancel — keep the item
    });
 
    cy.get("[data-testid='delete-btn']").click();
    cy.get("[data-testid='item']").should("be.visible");
  });
 
  it("accepts the native confirm, sees an HTML success modal, then dismisses an alert", () => {
    cy.on("window:confirm", () => true);             // OK on the confirm
    cy.on("window:alert", (text) => {
      expect(text).to.equal("Item deleted.");
    });
 
    cy.get("[data-testid='delete-btn']").click();
 
    // After the confirm, an HTML modal appears for a follow-up choice.
    cy.get("[data-testid='success-modal']").should("be.visible");
    cy.get("[data-testid='success-modal']")
      .find("[data-testid='close-modal']")
      .click();
 
    // Final native alert is auto-asserted by the cy.on listener above.
    cy.get("[data-testid='item']").should("not.exist");
  });
});

Three different interruption types in one test, each handled with the right tool: cy.on for the native dialogs, cy.get for the HTML modal.

⚠️ Common mistakes

  • Trying to cy.get('alert') on a native browser alert. Native dialogs are not in the DOM. cy.get finds zero elements and the test fails with "expected to find an alert." Use cy.on("window:alert", ...) for native dialogs and cy.get for HTML modals — never the other way around.
  • Forgetting that cy.stub(win, "prompt") must be set up before the code that calls window.prompt. If the click handler runs first, the real prompt blocks the test. Stub the prompt in a cy.window().then(...) block before the click.
  • Conditionally clicking with if ... cy.get(...).click() for elements that are expected to always exist. The if-then pattern is a legitimate escape hatch for genuinely optional UI (a cookie banner that's absent on returning visits). Using it for a button that should always be there masks real bugs — the test passes regardless of whether the button rendered. Reach for cy.get(...) with retry as the default.

🎯 Practice task

Drive every interruption type in real tests. 20-25 minutes.

  1. Visit https://the-internet.herokuapp.com/javascript_alerts. Set baseUrl: "https://the-internet.herokuapp.com". The page has three buttons that trigger an alert, a confirm, and a prompt.
  2. Create cypress/e2e/dialogs.cy.ts with a describe("JavaScript dialogs") and four tests:
    • alert — click "Click for JS Alert"; assert via cy.on("window:alert", ...) that the text is "I am a JS Alert"; assert the result text on the page reads "You successfully clicked an alert".
    • confirm OK — click "Click for JS Confirm"; assert the text and let the default OK fire; assert the result text reads "You clicked: Ok".
    • confirm Cancel — click "Click for JS Confirm"; return false from the window:confirm handler; assert the result text reads "You clicked: Cancel".
    • promptcy.window().then((win) => cy.stub(win, "prompt").returns("Hello there")), click "Click for JS Prompt", assert the result text reads "You entered: Hello there".
  3. Find any public site with a cookie banner (most major news sites have one). Write a quick test that visits, accepts the banner, and asserts the banner is not.exist. Then write a second test that pre-seeds the consent cookie with cy.setCookie and asserts the banner never appeared — confirming the pre-seed approach is faster.
  4. Stretch: in your test from step 3, refactor the cookie-banner click into a dismissCookieBanner custom command following the conditional pattern from the lesson. Confirm the command does nothing harmful when called on a page that has no banner (e.g., a follow-up page on the same site).

You can now drive every kind of page interruption. The next lesson finishes the chapter with file uploads and downloads — the last DOM corner where most teams hit a wall.

// tip to track lessons you complete and pick up where you left off across devices.