Q19 of 48 · Cypress
Why doesn't Cypress support multiple tabs natively, and what's the workaround?
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:
- Strip the
targetattribute 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');
- Visit the URL directly if you don't care about the click itself:
cy.visit('/help');
- Stub
window.openif 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');
});