QA How-To
How to Switch between tabs in Playwright (2026)
Learn playwright how to switch between tabs using popup events, context pages, reliable waits, assertions, cleanup, and production-ready TypeScript examples.
18 min read | 3,472 words
TL;DR
Start waiting for the new Page before the click that opens it, then use the returned Page object directly. Playwright does not switch a global driver: every tab is an independent Page in the same BrowserContext.
Key Takeaways
- Start waiting for the new Page before the click that opens it, then use the returned Page object directly. Playwright does not switch a global driver: every tab is an independent Page in the same BrowserContext.
- Prefer web-first assertions over manual polling where possible.
- Create event and response waits before the action that triggers them.
- Use accessible locators and stable product contracts.
- Keep loops and waits bounded for CI safety.
- Retain traces and targeted artifacts for useful failure diagnosis.
Start waiting for the new Page before the click that opens it, then use the returned Page object directly. Playwright does not switch a global driver: every tab is an independent Page in the same BrowserContext. This guide answers playwright how to switch between tabs with production-oriented Playwright Test patterns for TypeScript.
The examples focus on reliable synchronization, accessible locators, useful assertions, and diagnostics. Replace the illustrative URLs and accessible names with contracts from your application, while keeping the event ordering and assertion strategy intact.
TL;DR
| Decision | Recommended approach | Why |
|---|---|---|
| page.waitForEvent("popup") | A known opener creates the tab | Best ownership signal |
| context.waitForEvent("page") | Any page or script creates it | Useful for indirect creation |
| context.pages() | Inspect tabs that already exist | Snapshot only, not synchronization |
The core rule is simple: observe the outcome that matters, start event waits before the triggering action, and let Playwright's locator assertions retry. Never repair uncertainty with a fixed sleep.
1. playwright how to switch between tabs: Understand the Page model
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
2. Capture a tab without a race
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
3. Choose popup or context events
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
4. Wait for useful readiness
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
5. Assert the destination tab
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
6. Work with several tabs
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
7. Return to the original page
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
8. Handle tabs created indirectly
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
9. Use fixtures and helpers
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
10. Debug flaky popup tests
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
11. Design maintainable tab coverage
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
12. Apply playwright how to switch between tabs safely
Testing tabs and popup pages starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.
For this part of the workflow, the useful Playwright surfaces include page.waitForEvent("popup"), context.waitForEvent("page"), page.context().pages(), waitForLoadState(), and page.close(). Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.
Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.
Runnable TypeScript example
import { test, expect } from '@playwright/test';
test('opens documentation in a new tab', async ({ page }) => {
await page.goto('https://example.com');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Documentation' }).click();
const docsPage = await popupPromise;
await docsPage.waitForLoadState('domcontentloaded');
await expect(docsPage).toHaveURL(/docs/);
await expect(docsPage.getByRole('heading', { level: 1 })).toBeVisible();
await docsPage.close();
});// Replace the URL and accessible names with values from your application.
Run the example with npx playwright test. For framework setup, review the Playwright TypeScript framework guide, then strengthen locator choices with Playwright locator best practices. Teams troubleshooting intermittent runs can also use the Playwright flaky test guide.
Interview Questions and Answers
Q: What is the first principle you would explain for tabs and popup pages?
I would define an observable contract before selecting an API. The action alone is not proof. I would assert the state a user or downstream system depends on, using a locator or response that Playwright can retry safely.
Q: Why should a test avoid waitForTimeout()?
A fixed delay guesses how long the application needs. It wastes time when the UI is fast and still fails when CI is slower. I wait for a precise event, response, locator state, or polled condition with a meaningful timeout.
Q: How do you prevent races?
I create the promise for a relevant event or response before performing the action that can emit it. Then I await the action and the promise in a clear order. This prevents the application from completing before the listener exists.
Q: Which locators are most maintainable?
I prefer roles, accessible names, labels, and stable test IDs. These represent user-facing contracts and are less coupled to DOM structure. I use CSS only where it expresses a stable product contract.
Q: What belongs in a failure report?
The report should show the expected contract, actual state, relevant URL, and diagnostic artifacts. A retained trace is often more useful than a screenshot alone because it includes actions, DOM snapshots, console messages, and network activity.
Q: How would you make this coverage CI-safe?
I isolate data, bound waits and loops, avoid order dependence, and run against a controlled environment. I retain artifacts on failure and validate the same test under configured browser projects without adding browser-specific sleeps.
Common Mistakes
- Waiting a fixed number of milliseconds instead of waiting for evidence.
- Using brittle selectors tied to layout or generated classes.
- Checking only that an action succeeded, without asserting business state.
- Creating an event or response wait after the triggering action.
- Allowing unbounded loops that can hang a worker.
- Sharing mutable state between parallel tests.
- Hiding a product defect with retries or broad exception handling.
- Capturing secrets or customer data in traces and screenshots.
Treat each mistake as a review prompt. A stable suite is not one that never fails. It is one whose failures are reproducible, correctly classified, and rich enough to diagnose. Review helper abstractions periodically because a helper that hides event ownership or swallows assertions makes failures harder to understand.
Conclusion
The practical answer to playwright how to switch between tabs is to use Playwright's supported APIs, synchronize on observable outcomes, and assert the product contract with retrying locators. The exact command matters, but event ordering, isolation, and a meaningful final assertion matter more.
Start with one representative happy-path test, add a negative or boundary case, and run both repeatedly in the same CI configuration used by the team. Once those tests are trustworthy, extract only the repeated mechanics into a small helper or fixture.
Interview Questions and Answers
How would you design this test?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
How do you avoid flaky synchronization?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
Which locator strategy would you choose?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
How do you handle negative paths?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
What diagnostics do you retain?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
How do you keep the test maintainable?
I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.
Frequently Asked Questions
What is the recommended starting point for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
Should I use a fixed timeout for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
How should I choose locators for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
How do I debug a CI-only failure for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
Should I create a reusable helper for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
What should the final assertion prove for tabs and popup pages?
Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.
Related Guides
- How to Switch between tabs in Cypress (2026)
- How to Switch between tabs in Selenium (2026)
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Run tests in headed mode in Playwright (2026)
- How to Scroll to an element in Playwright (2026)
- How to Assert a downloaded PDF in Playwright (2026)