Reliably Implementing Cross-Tab and Multi-Window Testing
AI generated
PASS
expect()
Multi-Window · E2E Testing
Reliably Implementing Cross-Tab and Multi-Window Testing
How to control multiple browser contexts in Playwright, verify use cases like checkout-in-a-new-tab, and synchronize windows with each other

Once an application deliberately opens a new tab, say for an external payment provider like PayPal, or has to keep state in sync across several simultaneously open windows, the classic E2E testing model confined to a single window no longer cuts it. Cross-tab and multi-window testing demands a deliberate, explicit model of several simultaneously active browser contexts, one that Playwright supports considerably more naturally than Cypress, where the same task has historically only been solvable with substantial workarounds.

16 min read Multi-Window E2E Testing

1. Why multi-tab scenarios are especially hard to test

A single browser window has a clearly bounded, linearly traceable state: a page loads, a user performs an action, the page reacts. But the moment a second window or a second tab enters the picture, say because a link deliberately opens with target="_blank" or an external payment provider runs in its own window, two parallel but content-wise interdependent states now have to be tracked simultaneously in the test, which makes the test logic considerably more complex than a purely sequential flow.

This complexity shows up especially clearly around timing dependencies between windows: an action in the second tab, say completing a PayPal payment, has to reliably reach the first, original tab and trigger a state change there before the test can meaningfully continue in the original window, and the exact timing of these events across windows isn't always deterministically guaranteed in a real browser.

2. Deliberately controlling multiple browser contexts in Playwright

Playwright models a newly opened tab or window as its own Page object within the same BrowserContext, reliably captured via the original Page object's popup event once the application actually opens a new window. This clean, object-oriented model lets a test address both windows independently within the same test case, without having to manually switch between different window handles, the way older tools built on the WebDriver protocol still required.

For scenarios additionally needing separate, fully isolated sessions, say testing two different, simultaneously logged-in user accounts at once, Playwright also offers the option of creating several independent BrowserContext instances in parallel, which fundamentally don't share cookies, LocalStorage, or session state, unlike two tabs within the same context.


import { test, expect } from '@playwright/test';

test('PayPal redirect in a new tab completes checkout in the original tab', async ({ page, context }) => {
  await page.goto('/checkout/payment');

  const [popup] = await Promise.all([
    context.waitForEvent('page'),
    page.getByRole('link', { name: 'Pay with PayPal' }).click(),
  ]);

  await popup.waitForLoadState();
  await popup.getByLabel('Email').fill('customer@example.com');
  await popup.getByRole('button', { name: 'Log in and pay' }).click();
  await popup.waitForEvent('close');

  // Back in the original tab: checkout should now be complete
  await expect(page.getByText('Order placed successfully')).toBeVisible();
});

3. Use case: reliably verifying checkout-in-a-new-tab

A particularly practice-relevant use case for a Magento project is the external payment redirect, where the actual checkout tab deliberately stays open while a second tab or popup window handles authentication with the payment provider. A reliable test has to check not only that the second tab opens correctly and loads the right URL, but also that the original checkout tab actually transitions into the expected, successful end state once the external payment completes.

Extra caution is needed for payment providers that redirect within the same tab instead of opening a real popup and then redirect back to the application, since that behavior requires a fundamentally different test pattern than an actual two-window scenario. A cleanly written test should therefore first check which of the two patterns the given payment provider actually uses, before deciding on a testing strategy, instead of treating both patterns the same without thinking it through.

4. Reliably testing synchronization between windows

Beyond plain payment redirects, many modern web applications require state changes in one tab to automatically become visible in all other simultaneously open tabs of the same application, say when a product gets added to the cart in tab A and the cart count in tab B should update automatically, without a manual reload. Such synchronization technically usually relies on the BroadcastChannel API, on shared LocalStorage with a storage event listener, or on regular polling against the backend.

A reliable test for this behavior deliberately opens two Page objects within the same BrowserContext, deliberately performs the state-changing action in the first tab, and then explicitly waits in the second tab for the expected, asynchronously arriving change, instead of using a fixed wait time that, depending on the actual synchronization speed, either takes unnecessarily long or is occasionally too short, making the test flaky.


test('cart change synchronizes between two tabs', async ({ context }) => {
  const tabA = await context.newPage();
  const tabB = await context.newPage();

  await tabA.goto('/product/example-item');
  await tabB.goto('/checkout/cart');

  await tabA.getByRole('button', { name: 'Add to cart' }).click();

  // explicitly wait for the async synchronization, no fixed wait time
  await expect(tabB.getByTestId('cart-count')).toHaveText('1', { timeout: 5000 });
});

5. Cypress limitations for multi-tab scenarios

Cypress deliberately forgoes native multi-tab testing for architectural reasons, since the entire framework is built on the basic assumption that a test controls exactly one window, and a tab switch triggered by the application would undermine that basic model. Trying to open target="_blank" within a Cypress test either gets blocked by default or opens the target page within the same tab instead, which is unsuited for genuine multi-window assertions.

The common Cypress workaround strategy is deliberately mocking or stubbing the external payment provider for testing purposes, instead of following the actual tab switch, which keeps the test working within Cypress's architectural boundaries but no longer really checks the actual multi-window interaction end to end. For projects where genuine cross-tab behavior plays a business-critical role, Playwright is usually the considerably better-suited choice because of this limitation.

6. Common pitfalls in multi-window tests

A common mistake is proceeding with interactions immediately after opening a new tab, without explicitly waiting for it to fully load, say via waitForLoadState(), causing the test to fail occasionally but not reliably reproducibly, depending on how fast the external page actually responds on a given run.

A second, equally common pitfall is accidentally focusing the wrong window when a test manages several Page objects at once but accidentally runs an interaction against the original window instead of the newly opened one, leading to confusing, hard-to-trace error messages, since the called method formally exists but runs into nothing in the wrong context. Giving each Page object a descriptive variable name, say checkoutTab and paymentPopup instead of generic names like page2, already considerably reduces this risk.

7. Best practices for structuring multi-window tests

A well-structured multi-window test should explicitly and descriptively reference every window involved from the start, deliberately target all asynchronous wait operations at concrete, expected state changes instead of fixed time spans, and clearly mark the point where the window switches within the test code through a comment or a dedicated helper function, so later readers of the test can immediately spot where the interaction moves between windows.

For recurring multi-window patterns, say the PayPal redirect flow, a shared, reusable helper function is worth building that encapsulates opening the popup, filling out the external form fields, and waiting for the window to close, instead of repeating this multi-line flow redundantly in every single test case that performs a payment through that provider.

8. Testing parallel, isolated user sessions with multiple BrowserContext instances

Beyond testing several tabs within the same session, some scenarios call for two fully independent, simultaneously active user sessions, say to verify that an admin user cancels an order in a backend window while the same customer, in a second, fully isolated window, watches the order status update in the storefront at the same time. A single BrowserContext isn't enough for this scenario, since both windows would otherwise share the same cookies and the same session state.

Playwright solves this by creating two independent BrowserContext instances within the same test run, with each instance holding its own cookies, its own LocalStorage, and its own session identity, making it possible to test genuine two-user scenarios without having to run two entirely separate test executions or even two separate browser installations.


test('admin cancels an order, customer sees the status change live', async ({ browser }) => {
  const adminContext = await browser.newContext({ storageState: 'admin-session.json' });
  const customerContext = await browser.newContext({ storageState: 'customer-session.json' });

  const adminPage = await adminContext.newPage();
  const customerPage = await customerContext.newPage();

  await customerPage.goto('/sales/order/view/order_id/1001');
  await adminPage.goto('/admin/sales/order/view/order_id/1001');
  await adminPage.getByRole('button', { name: 'Cancel order' }).click();

  await customerPage.reload();
  await expect(customerPage.getByText('Canceled')).toBeVisible();

  await adminContext.close();
  await customerContext.close();
});

9. Multi-window testing approaches at a glance

The table below compares the cross-tab and multi-window testing approaches presented.

Approach Suited for Limitation
Playwright BrowserContext popup event Genuine multi-tab interactions Requires clean event handling
Multiple BrowserContext instances Parallel, isolated user sessions Higher resource usage
Cypress with mocking of the external tab Cypress projects without Playwright No genuine end-to-end multi-window
BroadcastChannel synchronization test Cross-tab state syncing Needs explicit waiting instead of a fixed span

Mironsoft

E2E test strategy, CI integration, and stable test suites

Test suites that actually find bugs instead of just blinking red?

We review existing E2E test suites for flakiness, missing test isolation, and inefficient CI runtimes, then build a test strategy that genuinely creates confidence instead of just checking a box.

Test Audit

Systematically uncovering flaky tests, testing pyramid gaps, and coverage blind spots.

CI Optimization

Building parallel execution, retry strategies, and fast feedback loops.

Cypress/Playwright Setup

Setting up robust E2E suites for Magento frontends from the ground up.

10. Summary

Cross-Tab Testing: The Essentials at a Glance

Core idea

Playwright models new tabs as their own Page objects within the same BrowserContext.

Key use case

External payment redirects like PayPal must reliably feed back into the original checkout tab.

Main pitfall

Missing explicit waits for window load or synchronization make tests flaky.

Cypress limit

Cypress doesn't architecturally support genuine multi-tab testing, only mocking as a workaround.

11. FAQ: Cross-Tab Testing: The Essentials at a Glance

1How do I catch a newly opened tab in Playwright?
Via the BrowserContext's popup event, combined with the action that actually triggers the new tab.
2Can Cypress run genuine multi-tab tests?
No, not natively for architectural reasons, the common workaround is mocking the external tab.
3How do I reliably test a PayPal redirect?
By catching the new tab, filling out the external form fields, and checking the end state in the original tab.
4How do I synchronize state between two tabs in a test?
By explicitly waiting for the expected, asynchronously arriving change instead of a fixed wait time.
5What's the difference between two tabs and two BrowserContext instances?
Two tabs share cookies and session, two BrowserContext instances are fully isolated.
6Why does my multi-window test sometimes randomly fail?
Usually due to a missing waitForLoadState() after opening a new window.
7How do I avoid mixing up multiple Page objects?
Through descriptive variable names like checkoutTab instead of generic names like page2.
8Is a helper function worth it for recurring multi-window flows?
Yes, it encapsulates opening, filling, and waiting and reduces redundancy across many test cases.
9Does the BroadcastChannel API work in all test browsers?
Generally yes in modern browsers, an explicit support check in the test still doesn't hurt.
10Should I prefer Playwright for multi-window tests?
Yes, if genuine cross-tab behavior is business-critical, Playwright is the considerably better-suited choice.