Opening a new tab or window in a web application and then interacting with it is a common user flow, but it can trip up Cypress tests if you’re not careful. The core issue is that Cypress commands, by default, operate within the context of the currently active tab. When a new tab opens, Cypress doesn’t automatically switch its focus.

Here’s how to manage multi-tab flows in your Cypress tests:

The Problem: Cypress Stays Put

When you click a link or button that triggers target="_blank" or uses JavaScript to window.open(), a new tab or window appears. Cypress, however, remains focused on the original tab. Any subsequent commands like cy.get(), cy.type(), or cy.click() will try to execute in the original tab, leading to errors because the elements you’re looking for are in the new tab.

The Solution: cy.origin() and cy.window()

Cypress v9.6.0 introduced cy.origin() to handle cross-origin scenarios, which is the primary mechanism for dealing with new tabs that navigate to a different origin. For same-origin new tabs, you can use cy.window().

Let’s break down how to use them:

Scenario 1: New Tab Navigates to a Different Origin

This is the most common and robust way to handle new tabs. If the new tab loads a URL on a different domain than your test’s baseUrl, you must use cy.origin().

Example: Your application has a link that opens a help page on a third-party support site.

describe('Multi-tab flow with different origin', () => {
  it('should navigate to a new tab on a different origin and interact', () => {
    cy.visit('http://localhost:8080'); // Your app's origin

    // Stubbing window.open to control the new tab's URL for demonstration
    // In a real scenario, the link would directly open the URL.
    cy.window().then((win) => {
      cy.stub(win, 'open').callsFake((url) => {
        win.open.restore(); // Restore stub after call
        setTimeout(() => {
          // Simulate opening a new tab with a different origin
          const newWin = window.open(url, '_blank');
          // You might need to manually set the URL if it doesn't navigate automatically
          // For testing, we often stub this behavior.
        }, 0);
      });
    });

    // Trigger the action that opens a new tab (e.g., clicking a link)
    cy.get('[data-cy="help-link"]').click();

    // Use cy.origin() to switch context to the new tab's origin
    // The first argument is the origin URL (e.g., 'https://support.example.com')
    // The second argument is a callback function where you perform actions in the new tab.
    cy.origin('https://support.example.com', () => {
      // Now you are in the context of the new tab's origin.
      // You can interact with elements on this new page.
      cy.get('h1').should('contain', 'Customer Support');
      cy.get('[data-cy="search-input"]').type('Password reset');
      cy.get('[data-cy="search-button"]').click();
    });

    // After cy.origin() finishes, Cypress automatically returns to the original tab's context.
    cy.get('[data-cy="user-profile"]').should('be.visible');
  });
});

How it works: cy.origin() tells Cypress to expect commands within the callback to be executed on a specific origin. Cypress handles the underlying complexities of managing the browser context and ensuring that commands within cy.origin() target the correct tab. The key is that cy.origin() waits for the new tab to load and be ready for interaction.

Scenario 2: New Tab Navigates to the Same Origin

If the new tab opens a URL on the same domain as your baseUrl, you can use cy.window() and a bit of manual work. cy.origin() is also capable of handling same-origin scenarios, but cy.window() is often simpler for this specific case.

Example: Your application has a "Preview" button that opens a new tab with a preview of the current item, on the same domain.

describe('Multi-tab flow with same origin', () => {
  it('should open a new tab on the same origin and interact', () => {
    cy.visit('http://localhost:8080/items/123'); // Your app's origin

    // Trigger the action that opens a new tab
    cy.get('[data-cy="preview-button"]').click();

    // Get a reference to all browser windows
    cy.window().then((originalWindow) => {
      // Find the new window. This can be tricky.
      // Often, you'll need to know something unique about the new window
      // or iterate through all windows to find the one you want.
      // A common way is to check `window.opener` or `window.name`.

      // Let's assume the new window has a specific title or URL pattern
      // that we can identify. For simplicity, we'll look for a window
      // that isn't the original window and has a URL containing '/preview'.
      cy.get('body').then(($body) => { // This gets the body of the *original* window
        const newWindow = Cypress.browser.windows.find(win =>
          win !== originalWindow && win.location.href.includes('/preview')
        );

        if (!newWindow) {
          throw new Error('Could not find the new preview window.');
        }

        // Switch Cypress's focus to the new window
        cy.wrap(newWindow).then((win) => {
          // Now, all commands will operate within this new window's context.
          cy.get('h1').should('contain', 'Item Preview');
          cy.get('[data-cy="item-title"]').should('contain', 'Awesome Widget');
        });
      });
    });

    // Cypress automatically returns to the original tab's context after the window block.
    cy.get('[data-cy="edit-button"]').should('be.visible');
  });
});

How it works:

  1. cy.window() initially gives you access to the original window object.
  2. We then need to find the new window. This is the trickiest part. You might need to listen for the window.open event, or more commonly, iterate through window.getAll() (a Cypress internal, but you can often find the window by inspecting its location.href or window.name after it’s opened).
  3. Once the new window is identified, cy.wrap(newWindow) switches Cypress’s focus to that specific window object.
  4. Subsequent commands will then execute within that new tab.
  5. When the cy.wrap(newWindow).then(...) block finishes, Cypress should revert to the original tab’s context. If it doesn’t, you might need to re-select the original window using cy.window() again.

Important Note on Finding the New Window: The exact method to find the newWindow can vary. You might need to:

  • Stub window.open: Intercept the call and get a reference to the opened window.
  • Use window.name: If the window.open call sets a name attribute (window.open(url, 'myNewWindowName')), you can search for that name.
  • Wait for URL Change: If the new tab takes a moment to load, you might need to wait for its location.href to match a pattern.

The Counterintuitive Lever: window.opener

When a new tab is opened by your application (e.g., via a link with target="_blank" or window.open()), the new window object has a property called window.opener. This opener property points back to the original window object that created it. You can use this to reliably identify the new window from within the new tab’s context if you need to perform actions and then signal back to the opener, or simply to confirm you’re in the right place. For example, within the cy.origin() callback or the cy.wrap(newWindow) block, you could assert cy.window().should('have.property', 'opener').

The Next Step

Once you’ve mastered handling multiple tabs, you’ll inevitably run into scenarios where a new tab opens, but Cypress struggles to detect it or switch context reliably. This often happens with complex JavaScript-driven pop-ups or when multiple tabs are opened in rapid succession.

Want structured learning?

Take the full Cypress course →