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 and consent modals — HTML, not native
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.getfinds zero elements and the test fails with "expected to find an alert." Usecy.on("window:alert", ...)for native dialogs andcy.getfor HTML modals — never the other way around. - Forgetting that
cy.stub(win, "prompt")must be set up before the code that callswindow.prompt. If the click handler runs first, the realpromptblocks the test. Stub the prompt in acy.window().then(...)block before the click. - Conditionally clicking with
if ... cy.get(...).click()for elements that are expected to always exist. Theif-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 forcy.get(...)with retry as the default.
🎯 Practice task
Drive every interruption type in real tests. 20-25 minutes.
- Visit
https://the-internet.herokuapp.com/javascript_alerts. SetbaseUrl: "https://the-internet.herokuapp.com". The page has three buttons that trigger an alert, a confirm, and a prompt. - Create
cypress/e2e/dialogs.cy.tswith adescribe("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:confirmhandler; assert the result text reads "You clicked: Cancel". - prompt —
cy.window().then((win) => cy.stub(win, "prompt").returns("Hello there")), click "Click for JS Prompt", assert the result text reads "You entered: Hello there".
- alert — click "Click for JS Alert"; assert via
- 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 withcy.setCookieand asserts the banner never appeared — confirming the pre-seed approach is faster. - Stretch: in your test from step 3, refactor the cookie-banner click into a
dismissCookieBannercustom 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.