Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Working with multiple pages and tabs

Core Playwright Concepts

Working with multiple pages and tabs

Managing multiple pages and tabs is essential for testing workflows where user actions trigger new windows or navigation. Understanding how to handle these browser contexts allows you to orchestrate interactions across several open documents simultaneously. You will use this technique whenever your application logic involves redirects, external authentications, or complex multi-window dashboard interactions.

The Context-Page Relationship

To understand how pages work, you must first recognize that a BrowserContext represents an isolated environment, much like an incognito session. Within this context, you can have multiple Page objects. When you open a URL, you are interacting with a specific tab. Playwright treats each tab as an independent Page instance, allowing you to manipulate them individually or concurrently. The internal architecture ensures that each page maintains its own document object model, cookies, and local storage, provided they reside within the same context. By default, the main window is created for you, but handling secondary windows requires capturing events that signal the creation of new pages. This architecture is intentional; it prevents side effects from leaking between tests while providing full control over the browser's orchestration, allowing developers to simulate complex user behaviors like clicking a link that opens in a new tab.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  // The initial page is created within the context
  const page = await context.newPage();
  await page.goto('https://example.com');
  console.log(await page.title());
  await browser.close();
})();

Handling New Tab Events

When a user clicks a link with target='_blank', the browser opens a new tab. In your automation script, you must explicitly listen for the 'page' event on your existing context to capture this new instance. The reason we use an event listener is that opening a tab is asynchronous; the script continues executing while the browser process handles the window creation. If you do not register this listener before triggering the click, the tab will open, but your code will have no reference to it, effectively leaving you stranded on the original page. By awaiting the promise returned by the 'page' event, you effectively synchronize your script's execution flow with the browser's UI state. This mechanism is crucial for ensuring that you always have a handle on the correct page object before you attempt to perform assertions or user interactions on the newly opened content.

const [newPage] = await Promise.all([
  context.waitForEvent('page'), // Wait for the tab to appear
  page.click('#open-new-tab') // Trigger that opens the tab
]);
await newPage.waitForLoadState(); // Ensure content is ready
console.log(await newPage.title());

Switching Between Pages

Once you have multiple Page objects stored as variables, switching between them is trivial because they are just JavaScript objects pointing to different browser tab resources. You do not need to perform any special 'focus' operations to navigate between them; you simply call methods on the specific variable you need. For example, if you need to compare data from an admin dashboard in one tab against a user-facing page in another, you can hold references to both simultaneously. The underlying engine keeps these pages active regardless of which one was last interacted with. This separation of concerns allows you to write clean, readable code where you invoke actions like 'click' or 'fill' on the target page instance. This paradigm of direct reference management is highly efficient, preventing the need for complex state management when orchestrating cross-page test scenarios in your suites.

// Both pages remain open and accessible
await page1.bringToFront(); // Optional visual focus
await page1.fill('#search', 'query');
await page2.bringToFront();
await page2.click('#submit-report');

Managing Page Lifecycles

Every page object consumes memory and browser resources. It is standard practice to close tabs as soon as their task is complete to prevent memory leaks and unpredictable behavior in large test suites. While the browser context handles the cleanup of all associated pages upon its own termination, explicitly calling page.close() is a defensive programming pattern that ensures the test environment remains lightweight. Think of the page object as a handle to a living DOM; once closed, any attempt to interact with it will throw an error. This is a deliberate design choice that informs you if your test is attempting to manipulate stale objects. Always ensure that your cleanup code resides in finally blocks or teardown hooks to guarantee that your test environment is reset to a clean state even if individual assertions fail during the middle of the process.

try {
  await newPage.goto('https://target.com');
  // Perform actions
} finally {
  await newPage.close(); // Clean up regardless of success
}

Accessing All Open Pages

Sometimes you may trigger multiple actions that lead to an unknown number of open tabs. Instead of waiting for specific events, you can query the BrowserContext for a list of all currently open pages. The context.pages() method returns an array of all active Page objects currently managed by the context. This is exceptionally powerful for debugging or for scenarios where the order of tab creation is non-deterministic. By iterating over this array, you can inspect the state of every open tab to find the specific one matching your criteria, such as checking a URL or verifying an element presence. Remember that the array order corresponds to the creation sequence, meaning index 0 is typically the first tab opened in that context. This feature provides a safety net for complex integrations where the UI might behave differently based on dynamic user session variables.

const allPages = context.pages();
for (const p of allPages) {
  const url = p.url();
  if (url.includes('dashboard')) {
    await p.bringToFront();
    break;
  }
}

Key points

  • A BrowserContext acts as an isolated sandbox that contains multiple Page instances.
  • The 'page' event must be caught using Promise.all to capture tabs opened by user interactions.
  • Each Page object acts as an independent handle to a specific browser tab.
  • Pages do not require explicit focus to be manipulated via automation scripts.
  • Closing pages after use is a critical step for maintaining low memory overhead during testing.
  • The context.pages() method provides a programmatic way to retrieve a list of all open tabs.
  • Direct references to page objects are the primary way to manage multi-tab application flows.
  • Wait for load states on new pages to ensure that network navigation has fully completed.

Common mistakes

  • Mistake: Interacting with a new page immediately after clicking a link. Why it's wrong: The page may not have finished opening or initialized. Fix: Use 'context.waitForEvent("page")' to await the new page instance.
  • Mistake: Assuming 'page' refers to the new tab/window after an action. Why it's wrong: Playwright keeps the 'page' object pointing to the original page instance. Fix: Store the returned value from the event listener as the new page object.
  • Mistake: Closing the browser context before performing operations on all pages. Why it's wrong: Closing a context terminates all pages within it, causing pending actions to fail. Fix: Ensure all page operations are finished or awaited before calling 'context.close()'.
  • Mistake: Using global locators that span across multiple pages. Why it's wrong: Playwright locators are scoped to specific pages, and trying to use a locator from Page A on Page B will result in confusion or failures. Fix: Always use the specific page object instance to generate locators for that page.
  • Mistake: Neglecting to switch context focus when handling popups. Why it's wrong: Actions performed by default go to the primary page, ignoring secondary windows. Fix: Explicitly define the target page variable and execute actions against that specific object.

Interview questions

How do you open a new page or tab in Playwright and interact with it?

To open a new page in Playwright, you typically use the context's 'newPage' method, which creates a fresh isolated browsing context. When dealing with links that open in new tabs, you listen for the 'page' event on the context using 'context.waitForEvent("page")'. You perform the action that triggers the tab, then wait for that event to resolve. This returns a Page object, which you then use exactly like any other page to perform actions like 'page.click()' or 'page.fill()'.

Why is it important to use browser contexts instead of just pages when testing multiple tabs?

Browser contexts are critical because they act as isolated incognito-like sessions. By using separate contexts, you ensure that cookies, local storage, and session data do not leak between your tests. If you attempt to manage multiple tabs within a single shared context, authentication state or cache issues from one tab might inadvertently affect the other, leading to flaky tests. Contexts provide a lightweight, performant way to achieve complete browser isolation, which is a fundamental requirement for reliable parallel test execution in Playwright.

How do you handle multiple pages when they are opened simultaneously by the application?

When an application opens multiple pages at once, you can access them through the 'context.pages()' method, which returns an array of all currently open pages. To target a specific one, you might iterate through the array or use 'waitForEvent' to capture them as they open. It is important to maintain references to these page objects in your script variables so you can switch focus between them seamlessly using standard method calls, ensuring your commands are sent to the correct active tab.

Compare the approach of using 'context.waitForEvent("page")' versus iterating through 'context.pages()'. When should you prefer one over the other?

The 'waitForEvent("page")' method is a reactive, event-driven approach best suited for predictable triggers, like clicking a link that opens a specific new tab. It is highly precise and prevents race conditions. In contrast, 'context.pages()' is a static snapshot that is more useful when you need to inspect the total state of the browser, such as verifying the number of open tabs or recovering after an unpredictable UI state. Use the event-based approach for active automation and the snapshot array for state verification or cleanup.

How can you synchronize actions across two different pages in Playwright?

Synchronization across pages requires careful coordination of the test script's execution flow. Since Playwright is asynchronous, you can trigger an action on the first page and use 'Promise.all' to wait for the second page to manifest simultaneously. For example, 'const [newPage] = await Promise.all([context.waitForEvent("page"), page.click("a[target=_blank]")])'. This ensures the code execution waits for the new page to be fully initialized before attempting to perform any further assertions or interactions on that specific tab, preventing errors caused by trying to access a page that does not yet exist.

Explain how to handle popups or new windows that might not behave like standard tabs, such as window.open calls.

Playwright handles 'window.open' calls by treating them as new pages within the existing context. To handle these, you must proactively set up a listener for the 'popup' event before the action occurs. Use 'page.waitForEvent("popup")' to capture the new window instance. This is essential because if you do not attach the listener before the trigger, the event might fire and be missed by your script. Once captured, the returned page object behaves like any other, allowing you to switch focus, set viewport sizes, or perform assertions on the newly opened window content.

All Playwright interview questions →

Check yourself

1. How do you correctly capture a new page opened by a user interaction in Playwright?

  • A.page.waitForNewPage()
  • B.context.waitForEvent('page')
  • C.browser.newPage()
  • D.page.on('popup')
Show answer

B. context.waitForEvent('page')
context.waitForEvent('page') is the recommended approach to asynchronously wait for and return the new page instance. Option 0 is not a valid method; option 2 creates a new blank page; option 3 is an event listener but is less concise than the waitForEvent pattern.

2. If you perform an action that triggers a new tab, why must you capture that tab via a promise-based event listener?

  • A.Because the new tab is a separate process
  • B.Because the browser needs to be restarted
  • C.Because the new tab may open before the code reaches the check
  • D.Because Playwright requires manual synchronization
Show answer

C. Because the new tab may open before the code reaches the check
A race condition exists where the tab opens after the click but before your next line of code executes. The waitForEvent mechanism ensures the code waits for the event to fire. The other options are incorrect interpretations of Playwright's architecture.

3. When working with multiple pages in the same browser context, what happens if you close the context?

  • A.Only the main page closes
  • B.All pages within that context are terminated
  • C.The browser stays open but pages refresh
  • D.The background processes continue indefinitely
Show answer

B. All pages within that context are terminated
A browser context is a container for pages; closing it closes everything inside it. Options 0, 2, and 3 are technically incorrect regarding the isolation provided by contexts.

4. Why should you avoid using a generic 'page' variable when juggling multiple browser tabs?

  • A.It leads to race conditions between page actions
  • B.It causes high memory usage
  • C.Playwright prevents using the same variable name
  • D.It breaks the test execution order
Show answer

A. It leads to race conditions between page actions
Overwriting a 'page' variable causes the script to lose the reference to the original tab, leading to errors when attempting to interact with the wrong window. The other options are not the primary reason for this best practice.

5. Which method is best suited for closing a specific page without ending the entire browser session?

  • A.context.close()
  • B.browser.close()
  • C.page.close()
  • D.page.reload()
Show answer

C. page.close()
page.close() specifically terminates the current page instance. context.close() terminates all pages in the context, and browser.close() ends the entire instance; page.reload() simply refreshes the current content.

Take the full Playwright quiz →

← PreviousPage navigation and history managementNext →Network request interception and mocking

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app