Q19 of 48 · Cypress

Why doesn't Cypress support multiple tabs natively, and what's the workaround?

CypressMidcypressmulti-tablimitationsmid

Short answer

Short answer: Cypress's in-browser architecture binds a test to a single tab; spawning a second tab would require a second runner instance. The standard workaround is to programmatically open the link in the same tab (`cy.visit`) or to verify the would-be navigation via `cy.window().its('open')`.

Detail

The architectural reason: Cypress runs inside the browser tab. A new tab is a new browser context, outside the runner's reach. Running a test that reaches into a second tab would mean either spawning a second runner (the implementation cost is high) or using a remote-control protocol (which is what Selenium/Playwright do, defeating Cypress's design).

The pragmatic stance: most "open in new tab" needs aren't really about needing the new tab — they're about verifying the link works. So the typical workaround is:

  1. Strip the target attribute before clicking, then assert on the resulting page in the same tab:
cy.get('a[href="/help"]').invoke('removeAttr', 'target').click();
cy.url().should('include', '/help');
  1. Visit the URL directly if you don't care about the click itself:
cy.visit('/help');
  1. Stub window.open if you only need to verify it was called with the right URL:
cy.window().then((win) => {
  cy.stub(win, 'open').as('windowOpen');
});
cy.get('[data-test=open-in-new]').click();
cy.get('@windowOpen').should('have.been.calledWith', '/help');

For genuinely multi-tab features (like inter-tab message passing or shared workers), Cypress is the wrong tool. Switch to Playwright or Selenium for that scenario specifically; many teams keep Cypress for the bulk of E2E and use Playwright for the multi-tab edge cases.

// EXAMPLE

external-link.cy.ts

it('opens help in a new tab (verified by stubbing)', () => {
  cy.visit('/');
  cy.window().then((win) => {
    cy.stub(win, 'open').as('open');
  });
  cy.get('[data-test=help-link]').click();
  cy.get('@open').should('have.been.calledWith', '/help', '_blank');
});

it('opens help (using removeAttr workaround)', () => {
  cy.visit('/');
  cy.get('[data-test=help-link]')
    .invoke('removeAttr', 'target')
    .click();
  cy.url().should('include', '/help');
});

// WHAT INTERVIEWERS LOOK FOR

Knowing the architectural reason (in-browser, single context), and naming all three workarounds — strip target, visit directly, stub `window.open`.

// COMMON PITFALL

Trying to use `cy.tab(...)` or similar invented APIs — Cypress simply doesn't expose multi-tab control.