This episode covers advanced interactions: handling iframes, popups, and multiple windows, drag and drop, uploads, keyboard events, testing single-page apps with dynamic content, and network stubbing with cy.intercept.

Tests are truly put to the test when the application has tricky things: third-party iframes, popups, new windows, drag and drop, file uploads, and dynamically changing content. Episode 8 walks you through these scenarios one by one with the right techniques.
This is the most "real-world work" episode so far. Once you master it, there are few web interactions you cannot automate.
Content inside an iframe is separate from the main DOM. For same-origin iframes, access them directly with cy.iframe() from the cypress-iframe plugin, or select manually with cy.get("iframe"). For cross-origin iframes, use cy.origin(), available since Cypress 12:
cy.origin("https://auth.example.com", () => {
cy.get("[data-cy=login-oauth]").click();
});cy.origin("https://auth.example.com", () => { ... }) lets Cypress run commands within another domain's context — overcoming the old "one domain per test" limitation. Note that arguments and variables used from outside must be passed explicitly through the function parameters.
Popups and new windows are a classic challenge. The key: do not rely on a new browser tab. Instead, intercept window.open and inspect its URL:
cy.window().then((win) => {
cy.stub(win, "open").as("openWindow");
});
cy.get("[data-cy=link-dokumentasi]").click();
cy.get("@openWindow").should("be.calledWith", "/docs");cy.stub(win, "open").as("openWindow") replaces the window.open function with a stub you can assert against. Instead of moving to a new tab that is hard to control, the test verifies that the application really calls open with the correct URL.
Drag and drop in the browser fires a sequence of events. The most reliable approach uses .trigger() to simulate the event sequence:
cy.get("[data-cy=item]").trigger("dragstart");
cy.get("[data-cy=dropzone]").trigger("drop");cy.trigger("dragstart") and cy.trigger("drop") fire DOM events directly. For drag-and-drop libraries that need coordinates, combine .trigger("dragstart", { clientX: 0, clientY: 0 }) followed by .trigger("drop", { clientX: 200, clientY: 200 }).
cy.selectFile lets you upload real files without extra plugins:
cy.get("[data-cy=input-avatar]").selectFile("cypress/fixtures/avatar.png");
cy.contains("Avatar terunggah").should("be.visible");cy.selectFile("cypress/fixtures/avatar.png") attaches the file to the upload input and triggers the change event. Keep sample files in the fixtures folder so tests do not depend on a developer's files.
For keyboard actions such as shortcuts:
cy.get("[data-cy=editor]").type("{ctrl}s");
cy.contains("Tersimpan").should("be.visible");cy.type("{ctrl}s") presses a key combination. Syntax like {enter}, {esc}, {backspace}, and {shift}tab is available for other needs.
SPAs (single-page apps) render content dynamically without a full page reload. This is an advantage: page state is relatively stable. What needs attention is elements that appear in alternation — spinners, skeletons, and swapped-out content.
cy.intercept("GET", "/api/produk").as("produk");
cy.get("[data-cy=tab-terbaru]").click();
cy.wait("@produk");
cy.get("[data-cy=list-produk]").children().should("have.length.gt", 0);Here cy.wait("@produk") waits for the latest data to finish loading before checking the list. Waiting on network events is the most deterministic way to handle dynamic content — far better than guessing animation durations.
cy.intercept is the most powerful tool for dynamic content: you can take full control of network responses:
cy.intercept("GET", "/api/produk/*", (req) => {
req.reply({ statusCode: 500, body: { error: "server down" } });
}).as("produkGagal");
cy.get("[data-cy=muat-produk]").click();
cy.contains("Gagal memuat produk").should("be.visible");req.reply({ statusCode: 500, body: ... }) forces the application to receive a server failure. Scenarios like retry and fallback flows can be tested without taking down a real server — we will dig deeper into this in episode 12.
Tip
Set up cy.intercept in beforeEach so all tests in one describe are consistent. Make sure the URL pattern is specific — an overly broad * can catch unwanted requests and cause tests to interfere with one another.
Episode 8 expanded the range of interactions: cross-origin iframes with cy.origin(), popups and new windows by stubbing window.open, drag and drop with trigger, uploads with cy.selectFile, keyboard events with cy.type, SPA dynamic content with cy.wait("@alias"), and full network control with cy.intercept.
Key takeaways:
cy.origin() unlocks testing on different domains.window.open so popups can be asserted without switching tabs.cy.selectFile handles real file uploads.cy.type("{ctrl}s") mimics keyboard shortcuts.In the next episode, episode 9, we will cover component testing — testing React, Vue, and Angular components in isolation, setting up the component environment, behavior-based component assertions, and Storybook integration. Your tests start moving down one level from full pages to components.