Resource library

QA How-To

How to Switch between tabs in Cypress (2026)

Learn cypress how to switch between tabs using href assertions, target blank handling, window.open stubs, cy.visit, and cy.origin without fake tab APIs.

22 min read | 2,780 words

TL;DR

Cypress cannot natively switch OS tabs. Assert new-tab intent (target or window.open), then cy.visit the URL in the controlled tab (with cy.origin if cross-origin). Stub window.open for scripted opens. Do not invent tab-switch commands.

Key Takeaways

  • Cypress does not provide a first-class switch-to-tab API; it controls one tab.
  • For target=_blank links, assert href/target/rel and visit the destination in-tab.
  • Stub window.open in onBeforeLoad to assert script-opened URLs.
  • Use cy.origin when destination content lives on another origin.
  • Distinguish in-app ARIA tabs from real browser new-tab behavior.
  • Prefer OAuth and popup test seams over brittle multi-window automation.
  • Centralize environment-specific external URLs to keep CI stable.

The short answer to cypress how to switch between tabs is that Cypress does not drive multiple OS-level browser tabs the way a manual user might. Cypress runs in a single controlled tab (with multi-origin support via cy.origin when needed). When an app opens a new tab with target="_blank" or window.open, the reliable patterns are: assert the URL, remove or rewrite the new-tab behavior, visit the URL in the same tab, or stub window.open. You test the user outcome without a second tab supervisor.

This surprises people who come from Selenium's window handles. The Cypress model optimizes determinism and control inside one tab. This guide explains why, shows current patterns for links and window.open, covers cross-origin cases, compares approaches, and prepares you for interview questions about tabs, popups, and multi-window claims.

TL;DR

App behavior Recommended Cypress approach Avoid
<a target="_blank" href="/help"> Assert href + target, then cy.visit the href Trying to "switch tab" natively
window.open(url) Stub window.open, assert call, optionally cy.visit(url) Assuming a second tab is controllable
Same-origin path change Stay in-tab; normal Cypress commands Overusing cy.origin
Cross-origin destination cy.origin after navigation strategies Inventing fake tab APIs
Auth popup windows Prefer test seams / API login / stubs Fragile multi-window hacks
Download links Treat as download testing, not tabs Confusing downloads with tab switches

Design tests around outcomes (correct URL opened, content reachable), not around OS tab chrome.

1. Cypress How to Switch Between Tabs: Why There Is No Tab Switch API

Cypress architecture attaches to a single browser tab it controls. That design reduces flake from background tab throttling, unclear focus, and racey window handles. There is no supported first-class command like "switch to tab index 2" comparable to classic WebDriver window handles.

So when a product manager says "the app opens a new tab," your job as an SDET is to translate that into testable contracts:

  1. Did the UI offer the correct URL?
  2. Did it request a new browsing context (target="_blank", window.open)?
  3. Is the destination content correct when loaded?
  4. Are rel security attributes present when required (noopener, noreferrer)?

You can answer all four without driving two tabs. That is the core of cypress how to switch between tabs in professional Cypress work.

If you truly need multi-page parallel contexts for exotic cases, you are often outside Cypress's sweet spot and should reconsider architecture or tool choice. For ordinary SaaS apps, the patterns below are enough.

2. Handle target="_blank" Links Without Leaving the Tab

it('opens docs in a new tab intent and loads the guide in-tab for assertions', () => {
  cy.visit('/settings/integrations')

  cy.get('[data-cy=docs-link]')
    .should('have.attr', 'href', '/docs/integrations')
    .and('have.attr', 'target', '_blank')
    .and('have.attr', 'rel')
    .and('include', 'noopener')

  // Exercise destination in the same tab under Cypress control
  cy.get('[data-cy=docs-link]').then(($a) => {
    const href = $a.prop('href') // absolute URL
    cy.visit(href)
  })

  cy.contains('h1', 'Integrations').should('be.visible')
})

Why this works:

  • You prove the product asked the browser for a new tab via target.
  • You prove security-related rel values when relevant.
  • You still fully assert destination content under Cypress control with cy.visit.

Alternative: rewrite the link before click to stay in the same tab:

cy.get('[data-cy=docs-link]')
  .invoke('removeAttr', 'target')
  .click()

cy.location('pathname').should('eq', '/docs/integrations')

Use rewrite-when-click when you specifically want to follow the click path (analytics handlers on click). Use href + visit when you want a cleaner separation between "link contract" and "destination page contract."

3. Stub window.open for Script-Initiated New Tabs

Many apps open tabs programmatically:

// application code (illustrative)
window.open('/reports/42', '_blank', 'noopener')

Test pattern:

it('opens the report preview via window.open', () => {
  cy.visit('/reports', {
    onBeforeLoad(win) {
      cy.stub(win, 'open').as('windowOpen')
    },
  })

  cy.get('[data-cy=preview-report-42]').click()

  cy.get('@windowOpen').should(
    'have.been.calledWithMatch',
    '/reports/42',
    '_blank',
  )

  // Optionally assert destination content in the controlled tab
  cy.visit('/reports/42')
  cy.get('[data-cy=report-title]').should('contain', 'Q2 Revenue')
})

Notes:

  • Stub in onBeforeLoad so application code cannot capture the real open early.
  • Alias the stub and assert arguments: URL, target, features string if your contract needs it.
  • Visiting the URL afterward validates the destination independently.

If window.open returns a Window reference the app immediately mutates, you may need the stub to return a carefully shaped object. Prefer product designs that do not require elaborate fake windows; they are hard to maintain.

For stubbing details more generally, see Cypress stub and spy examples.

4. Same-Origin Navigations Are Not Tab Switching

Teams sometimes call any second screen a "tab" in the product UI (in-app tabs). Those are ordinary DOM patterns:

it('switches to the Permissions in-app tab', () => {
  cy.visit('/projects/prj_1')
  cy.get('[role=tab]').contains('Permissions').click()
  cy.get('[role=tabpanel]').should('contain', 'Who can edit')
})

This is not a browser tab problem. Do not apply window.open stubs to ARIA tabs. Classify the UI first:

  • In-app tabs: roles, panels, URL query params
  • Browser new tab: target="_blank" or window.open
  • New window features: popup-like window.open with size features
  • Cross-origin redirect: may need cy.origin

Misclassification causes over-engineering.

5. Cross-Origin Destinations With cy.origin

When the new tab would load another origin (IdP, billing portal, docs site on another domain), Cypress requires cy.origin for commands on that origin after you navigate there:

it('reaches the external billing portal page content', () => {
  cy.visit('/billing')

  cy.get('[data-cy=open-billing-portal]')
    .should('have.attr', 'href')
    .then((href) => {
      const url = String(href)
      cy.visit(url)
    })

  cy.origin('https://billing.partner.example', () => {
    cy.contains('h1', 'Customer portal').should('be.visible')
  })
})

Adapt domains to your real environment. The point is not tab switching; it is origin isolation. For deeper coverage of cross-domain patterns, read Cypress cy.origin cross-domain examples.

If the external site cannot be tested directly (captcha, legal constraints), assert only the URL contract and status via cy.request where appropriate, or use a stubbed partner environment.

6. Compare Approaches for "New Tab" Requirements

Approach Proves link intent Proves destination UI Cross-origin ready Complexity
Assert href/target/rel only Yes No N/A Low
removeAttr('target') + click Partial Yes (same tab) Needs origin handling Low
href + cy.visit Yes if asserted Yes Yes with cy.origin Low
Stub window.open Yes (call args) Only if you visit Yes with visit+origin Medium
True multi-tab driver Yes Yes Tool-dependent High, not Cypress-native

Pick the cheapest row that matches the risk. Many "opens in new tab" acceptance criteria only need href/target/rel plus one destination content check in a controlled navigation.

7. Security Attributes, Opener Risks, and What to Assert

New tabs can retain access to window.opener unless mitigated. Product code often sets rel="noopener noreferrer" on external links. Tests should lock that contract:

cy.get('[data-cy=external-status-page]')
  .should('have.attr', 'target', '_blank')
  .should('have.attr', 'rel')
  .and('match', /noopener/)

You are not re-implementing browser security in Cypress; you are ensuring the app requests the safe pattern. Combine with periodic security review outside E2E.

For window.open, assert the target and features only when the app specifies them intentionally. Over-asserting brittle feature strings creates noise.

8. Popups, OAuth Windows, and Practical Test Seams

OAuth "login with popup" flows are the classic multi-window pain. Prefer seams:

  1. Test-mode redirect instead of popup when Cypress.env('OAUTH_TEST_MODE') is set.
  2. API-side identity injection via cy.session and backend test helpers.
  3. Stub the popup opener and simulate postMessage if the app architecture allows.
it('completes oauth via test seam without a real popup', () => {
  cy.visit('/login', {
    onBeforeLoad(win) {
      cy.stub(win, 'open').callsFake((url: string) => {
        // Simulate immediate redirect callback path used in test mode
        win.location.href = '/login/oauth/callback?code=test'
        return null
      })
    },
  })

  cy.get('[data-cy=oauth-google]').click()
  cy.location('pathname').should('eq', '/dashboard')
})

This is intentionally application-specific. The durable lesson: multi-window auth is a design problem first. Cypress tests should consume a stable seam, not reverse-engineer popup races every sprint.

9. Cypress How to Switch Between Tabs in Component Versus E2E Tests

Component tests rarely need real new tabs. If a component renders an external link, assert the attributes in isolation:

mount(<DocsLink href="https://example.com/docs" />)
cy.get('a')
  .should('have.attr', 'target', '_blank')
  .and('have.attr', 'rel')

Leave full destination navigation to a small number of E2E tests. That split keeps component suites fast and still protects the contract that matters at unit edges.

E2E remains the place for "user clicked help and the guide content is correct" with cy.visit after attribute checks.

10. Debugging Failed New-Tab Tests

Common failure modes:

  1. Absolute vs relative href: prop('href') vs attr('href') differ; be consistent.
  2. Stub installed too late: app cached window.open.
  3. Cross-origin without cy.origin: commands fail with origin errors.
  4. Click rewritten links that rely on React routers: ensure client routing still works after removeAttr('target').
  5. Popup blockers: real window.open from non-user gestures may be blocked; stubs avoid that class of flake.
  6. Feature environments: docs URL differs per env; build expected href from Cypress.env or config.

Debug with a single spec in open mode, inspect the anchor attributes, and log stub call args. Isolation tips live in how to run a single test in Cypress.

11. When Teams Insist on "Real Tabs" Anyway

Sometimes a compliance script literally requires a second tab. Options:

  1. Negotiate the acceptance criteria toward outcomes Cypress can guarantee.
  2. Use a second tool for that one scenario if legally required.
  3. Automate at a lower layer (API + contract tests for the URL generator).

Do not build an unsupported tab-switching framework on top of Cypress internals. It will break across browser versions and frustrate upgrades. Document the limitation calmly with a diagram of single-tab control; most stakeholders accept outcome-based testing when shown the flake risk of multi-tab automation.

12. Patterns Library for Common Product Stories

Story: Help link in footer

cy.get('footer a[data-cy=help]').should(($a) => {
  expect($a).to.have.attr('target', '_blank')
  expect($a.attr('href')).to.match(/\/help$/)
})

Story: Export opens report route via window.open

cy.visit('/exports', {
  onBeforeLoad(win) {
    cy.stub(win, 'open').as('open')
  },
})
cy.get('[data-cy=export-pdf]').click()
cy.get('@open').should('have.been.called')

Story: In-app editor tabs

cy.get('[role=tab]').contains('YAML').click()
cy.get('[data-cy=yaml-editor]').should('be.visible')

Story: External status page

cy.get('[data-cy=status-link]').then(($a) => cy.visit($a.prop('href')))
cy.origin('https://status.example.com', () => {
  cy.contains('All Systems Operational').should('exist')
})

Copy the story that matches your risk; do not force one pattern onto every link.

13. CI Stability and Environment URLs

New-tab tests often fail in CI because docs and partner portals differ by environment. Centralize URLs:

// cypress/support/urls.ts
export const docsIntegrations = () =>
  `${Cypress.env('DOCS_ORIGIN')}/integrations`
cy.get('[data-cy=docs-link]')
  .should('have.attr', 'href', docsIntegrations())

Seed DOCS_ORIGIN in config per environment. Hardcoded production docs links in staging tests are a classic source of false failures.

Also skip partner origin checks when the partner is down using documented conditional skips from your suite policy. That is suite stewardship, not weakness. See how to skip and group tests in Cypress for skip governance.

14. Accessibility Notes for Links That Open New Contexts

Opening a new browsing context can disorient users if unannounced. Some products add accessible names such as "Help (opens in a new tab)". When that is part of your UX standard, assert it:

cy.get('[data-cy=docs-link]')
  .should('have.attr', 'aria-label', 'Integrations docs (opens in a new tab)')

These assertions document inclusive behavior alongside target checks. They still do not require a second tab.

Tab Strategy Checklist

Classify in-app tabs versus browser new tabs, assert href/target/rel contracts, stub window.open when scripts open contexts, visit destinations under Cypress control, use cy.origin for other origins, prefer auth seams over popup automation, centralize env-specific URLs, and avoid unsupported multi-tab frameworks. Review any test that claims to "switch tabs" during code review and rewrite it to an outcome-based pattern. This checklist keeps Cypress honest and your suite stable.

Train PMs and designers to write acceptance criteria as "user can reach the guide content" plus "link indicates new tab," not "automation switches to tab 2." Language upstream prevents impossible QA commitments downstream. When interviewers ask how Cypress switches tabs, lead with architecture, then patterns, then a quick example. That answer signals senior judgment more than pretending a hidden API exists. Keep a living doc in the repo titled "New tab testing patterns" with the four stories above so people paste proven code instead of inventing brittle hacks during on-call hotfixes and late release freezes when judgment is rushed.

If you maintain a design system Link component that defaults target="_blank" for external hrefs, put the attribute tests next to that component and keep only a few E2E journeys for critical exits. Layering reduces duplication and keeps tab-related coverage scalable as the app grows. Revisit the strategy when you adopt multi-origin microfrontends; cy.origin usage may increase, but the "no tab switch API" rule remains. Architecture evolves; Cypress single-tab control remains the constraint you design tests around thoughtfully every planning cycle for web platforms.

15. Stakeholder Communication and Interview Framing

When a hiring manager or stakeholder asks whether your Cypress suite "covers multi-tab," answer in layers. First, explain the single-tab control model in one sentence. Second, show how you still cover business risk: correct URLs, new-context intent, destination content, and security attributes. Third, mention the rare cases you deliberately leave to another tool or manual charter. That layered answer is more credible than either "Cypress cannot do tabs" (too absolute and unhelpful) or "we switch tabs with a custom plugin" (often unmaintainable).

For internal training, create a 10-minute demo: open a target="_blank" link test, show the failing naive click, then apply removeAttr('target') and the href-visit variant. Engineers remember the demo when they write the next help-link test. Pair the demo recording with the living patterns doc so night-shift hotfix authors do not invent a fifth approach under pressure.

Finally, track how many tests in your suite still try to manipulate multiple windows. A yearly cleanup day that rewrites them to outcome-based patterns pays for itself in flake reduction. Multi-tab fantasies are not free; they cost CI minutes and human trust. Replace them with contracts users feel: the right page, at the right URL, with the right safety attributes, reachable from the control they clicked. That is the professional standard for cypress how to switch between tabs in 2026, and it is fully achievable without pretending Cypress is a multi-window desktop robot. Add this framing to onboarding checklists so new hires hear the architecture story before they search Stack Overflow for non-existent APIs and paste brittle snippets into critical billing or auth paths during their first sprint.

16. Release Checklist for New-Tab Coverage

Before a major release, scan for new target="_blank" links and new window.open calls in the product diff. For each one, confirm there is either a component-level attribute test, an E2E destination check, or an explicit decision that the risk is covered elsewhere. Unowned external exits are a common production surprise: a marketing banner ships with a broken docs URL and nobody notices until support tickets arrive.

Also verify that staging DOCS_ORIGIN and partner portal hosts resolve. A green suite that never visits the real staging docs host can miss DNS or TLS mistakes. At least one scheduled job should exercise the critical external exits against the true staging endpoints with cy.origin where required. Keep that job separate from the ultra-fast PR smoke if partner latency is high, but do not delete it from the release train.

When a new-tab test fails in that job, triage with the same discipline as any other flake: capture the exact href, confirm environment config, confirm whether the partner changed paths, and only then change assertions. Do not weaken security attribute checks to silence a failure. If noopener disappeared from the markup, that is a product regression worth failing the build for. Outcome-based tab testing is only valuable when teams treat the contracts as real user protection, not as optional ceremony to skip under schedule pressure during the final days of a release freeze or hotpatch weekend.

Interview Questions and Answers

Q: How do you switch between browser tabs in Cypress?

I generally do not. Cypress controls one tab. I assert new-tab intent via target or window.open, then navigate with cy.visit (and cy.origin if needed) to assert destination content.

Q: How do you test a link with target="_blank"?

I assert href, target, and often rel, then either remove target and click or cy.visit the href to validate the destination in the controlled tab.

Q: How do you handle window.open?

I stub window.open in onBeforeLoad, assert it was called with the expected URL and target, and optionally visit that URL to assert UI.

Q: When do you use cy.origin for tab-like flows?

When the destination is another origin and I need to run Cypress commands there after navigation. Origin isolation is separate from OS tabs.

Q: How are in-app tabs different?

They are DOM components with roles and panels. I click the tab control and assert the panel. No browser tab APIs are involved.

Q: Why doesn't Cypress support Selenium-style window handles?

Cypress prioritizes determinism in a single controlled tab. Multi-window automation is a common flake source and fights that model.

Q: How do you test OAuth popups?

I prefer test seams: redirect mode, API login, or a stubbed open that simulates callback. I avoid brittle real popup window switching.

Common Mistakes

  • Searching for a non-existent cy.switchToTab API.
  • Clicking target="_blank" links and expecting Cypress to follow the new tab automatically.
  • Stubbing window.open after the app already cached a reference.
  • Treating ARIA tabs as browser tabs.
  • Hardcoding production URLs for docs and partner portals.
  • Overusing { force: true } instead of fixing navigation contracts.
  • Building unsupported multi-tab frameworks on Cypress internals.
  • Ignoring rel="noopener" requirements on external links.
  • Trying to automate real OAuth popups without a test seam.
  • Asserting only that window.open was called and never checking destination content when content risk is high.

Conclusion

For cypress how to switch between tabs, stop trying to drive OS tabs and start testing the contract: correct URL, correct new-context intent, safe rel attributes, and correct destination content under Cypress control. Use stubs for window.open, attribute assertions for anchors, cy.visit for destinations, and cy.origin for other origins.

Pick one target="_blank" link in your app today, rewrite its test to the href-plus-visit pattern, and delete any pseudo tab-switching hacks. Your suite will be clearer, faster to debug, and aligned with how Cypress actually works in 2026.

Interview Questions and Answers

Does Cypress support multiple tabs like Selenium?

Not in the same window-handle style. Cypress is built around a single controlled tab for determinism. I validate new-tab intent and destination outcomes instead of switching handles.

Walk through testing target=_blank.

I assert href, target, and rel, then either remove target and click or visit the absolute href. I assert destination content in the controlled tab, using cy.origin when the domain differs.

How do you verify window.open without a second tab?

I stub window.open before app code runs, assert call arguments, and separately visit the URL to validate UI. That splits intent from destination correctness cleanly.

When is cy.origin required in these flows?

When I need to interact with or assert DOM on another origin after navigation. Superdomain changes are not solved by pretending a second tab exists.

How do you keep external link tests stable across environments?

I centralize docs and partner base URLs in Cypress.env, assert against those helpers, and avoid hardcoding production hosts in staging suites.

What is the risk of automating real OAuth popups?

Popup blockers, focus races, cross-origin constraints, and provider UI changes make them brittle. Test seams give deterministic auth coverage for the product paths we own.

How do you explain Cypress tab limitations to a non-technical stakeholder?

I explain we prove the app asks for a new tab and that the destination works, which matches user value. Driving OS tab chrome adds flake without improving confidence for most web products.

Frequently Asked Questions

Can Cypress switch between browser tabs?

Not as a first-class multi-tab driver. Cypress controls one tab. Test new-tab intent and destination content using attribute checks, stubs, cy.visit, and cy.origin.

How do I test a link that opens in a new tab?

Assert href, target=_blank, and rel as needed. Then remove the target and click, or cy.visit the href to assert destination content under Cypress control.

How do I stub window.open in Cypress?

Pass onBeforeLoad to cy.visit and call cy.stub(win, 'open').as('windowOpen'). Click the control and assert the stub arguments.

What if the new tab is a different domain?

Navigate to the URL under test, then wrap destination commands in cy.origin for that origin. Cross-origin rules still apply even though you are not switching OS tabs.

How do I test in-app tabs?

Treat them as DOM: click the tab control (often role=tab) and assert the corresponding panel content or route state.

Why did my target=_blank click not assert the new page?

Cypress stayed in the original tab. Rewrite target, or visit the href explicitly, instead of expecting automatic control of a second tab.

How should OAuth popups be tested?

Use a test seam such as redirect mode, API-assisted login, or a stubbed window.open that simulates the callback. Avoid fragile real popup switching.

Related Guides