Chapter 11 – Handling Multiple Pages, Tabs, and Frames

Modern web applications often involve multiple pages, pop-ups, or embedded frames. Whether it’s handling an OAuth login window, a payment gateway, or an embedded chat widget, understanding how to control multiple browser contexts is essential for building advanced and realistic Playwright tests.

In this chapter, we’ll go from basic page and tab management to advanced frame handling, including real-world examples, synchronization tips, and best practices for multi-page workflows.

1. Understanding Pages, Tabs, and Frames

Before diving into code, let’s clarify what each of these terms means in Playwright:

  • Page: A single browser tab or window.
  • Context: A browser session that can contain multiple pages.
  • Frame: A sub-document within a page (like an <iframe>).

Each page or frame has its own DOM and can be interacted with independently.

2. Working with multiple pages (tabs)

By default, each Playwright test runs in a single page. But you can easily open and switch between multiple tabs.

Example: Opening a new tab

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

test('open multiple pages', async ({ browser }) => {
  const context = await browser.newContext();

  const page1 = await context.newPage();
  await page1.goto('https://example.com');

  const page2 = await context.newPage();
  await page2.goto('https://playwright.dev');

  console.log('Page 1 title:', await page1.title());
  console.log('Page 2 title:', await page2.title());

  await context.close();
});

Here we:

  • Created a new browser context.
  • Opened two independent pages (tabs).
  • Navigated to different URLs.

Each page is isolated and actions on one don’t affect the other.

3. Switching between tabs dynamically

Sometimes, a new tab opens after clicking a link. You can wait for new pages to appear using context.waitForEvent().

Example:

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

test('handle tab opening', async ({ context }) => {
  const page = await context.newPage();
  await page.goto('https://example.com');

  // Assume clicking a link opens a new tab
  const [newPage] = await Promise.all([
    context.waitForEvent('page'), // Wait for new tab
    page.click('a[target="_blank"]') // Action that opens it
  ]);

  await newPage.waitForLoadState();
  console.log('New page title:', await newPage.title());

  await expect(newPage).toHaveURL(/example/);
});

This approach ensures your test doesn’t continue until the new page has fully loaded.

4. Handling pop-ups and new windows

Playwright treats pop-ups as separate pages. They appear in the same context but require explicit handling.

Example: Handling a popup window

const [popup] = await Promise.all([
  page.waitForEvent('popup'), // Wait for popup
  page.click('#open-popup') // Trigger popup
]);

await popup.waitForLoadState();
console.log('Popup URL:', popup.url());
await popup.close();

Pop-ups behave just like tabs and you can interact with them the same way.

5. Passing data between pages

Each page in a context shares the same cookies, storage, and session. That means you can log in once in one tab and remain logged in in another.

Example:

const context = await browser.newContext();
const loginPage = await context.newPage();
await loginPage.goto('https://app.example.com/login');
await loginPage.fill('#username', 'user');
await loginPage.fill('#password', 'pass');
await loginPage.click('button[type="submit"]');

// Open a new page with the same session
const dashboardPage = await context.newPage();
await dashboardPage.goto('https://app.example.com/dashboard');
await expect(dashboardPage.locator('h1')).toHaveText('Dashboard');

Tip: Use the same browser context to share session data. Use different contexts for isolated sessions.

6. Managing multiple browser contexts

You can create multiple browser contexts to simulate multiple independent users or sessions.

Example:

const user1Context = await browser.newContext();
const user2Context = await browser.newContext();

const user1 = await user1Context.newPage();
const user2 = await user2Context.newPage();

await user1.goto('https://example.com');
await user2.goto('https://example.com');

await user1.fill('#username', 'Alice');
await user2.fill('#username', 'Bob');

console.log('Two separate sessions running.');

Each context behaves like a separate browser instance and perfect for testing multi-user scenarios.

7. Handling frames (iframes)

Frames (or iframes) are separate HTML documents embedded within another page. They’re common in payment forms, ads, and embedded widgets.

You can access frames using page.frame().

Example:

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

test('interact with iframe', async ({ page }) => {
  await page.goto('https://www.w3schools.com/html/html_iframe.asp');

  const frame = page.frame({ url: /default.asp/ });
  await frame.click('a[href="/html/default.asp"]');

  await expect(frame.locator('h1')).toHaveText('HTML Tutorial');
});

In this example, we:

  • Found the frame by its URL pattern.
  • Performed actions within that frame context.

8. Locating frames by name or selector

If you know the name or selector of an iframe, you can access it easily:

const frame = page.frame({ name: 'frameName' });
// or
const frameElement = await page.frameLocator('#iframeId');
await frameElement.locator('button').click();

Tip: Use frameLocator() when chaining locators, it’s more reliable than manually switching frame contexts.

9. Nested frames

Frames can be nested, it is a frame inside another frame. You can drill down using Playwright’s frameLocator() chaining.

Example:

const nestedFrame = page.frameLocator('#outerFrame').frameLocator('#innerFrame');
await nestedFrame.locator('button.submit').click();

Playwright automatically handles the frame hierarchy for you.

10. Waiting for frames to load

Frames often load asynchronously, so you should wait for them to be available before interacting.

Example:

await page.waitForSelector('iframe');
const frame = page.frame({ name: 'paymentFrame' });
await frame.waitForSelector('#card-number');
await frame.fill('#card-number', '4111111111111111');

Always wait for the frame or its elements before performing actions.

11. Handling cross-domain iframes

When frames load content from a different domain, Playwright can still interact with them provided they are accessible to the test.

Example:

const frame = page.frame({ url: /paypal.com/ });
await frame.fill('#email', 'user@example.com');

If the iframe is sandboxed or protected by CORS, you may need to test in a staging environment that allows access.

12. Synchronizing multi-page and frame interactions

When handling multiple tabs or frames, synchronization becomes critical. Always wait for events like page.waitForLoadState() or frame.waitForSelector().

Example:

const [popup] = await Promise.all([
  page.waitForEvent('popup'),
  page.click('#openPopup')
]);
await popup.waitForLoadState('domcontentloaded');
await popup.fill('#email', 'test@example.com');

Use Promise.all() to wait for both the popup and the triggering action.

13. Debugging multi-page or frame issues

Debugging complex browser interactions can be tricky. Use these Playwright tools:

  • Inspector: npx playwright test --debug
  • Trace Viewer: npx playwright show-trace trace.zip
  • Console logs: page.on('console', msg => console.log(msg.text()));

You can also visually highlight frames:

await page.locator('iframe').evaluate(frame => frame.style.border = '3px solid red');

14. Best practices

  1. Keep track of your active page references.
  2. Use Promise.all() to handle events that open new pages or frames.
  3. Prefer frameLocator() for reliable iframe interactions.
  4. Avoid fixed timeouts, use waitForLoadState() instead.
  5. Use separate contexts for independent users.
  6. Reuse the same context for multi-tab workflows.

Exercise

Task 1: Write a test that opens two tabs and verifies titles on both.
Task 2: Create a test that interacts with an iframe using frameLocator().
Task 3: Simulate two users with different browser contexts.
Bonus: Handle a popup login window and verify a success message.

Cheat Sheet

ConceptCode Example
Open new pageconst page = await context.newPage();
Wait for new tabcontext.waitForEvent('page')
Handle popuppage.waitForEvent('popup')
Access framepage.frame({ name: 'myFrame' })
Use frame locatorpage.frameLocator('#frameId')
Wait for frameframe.waitForSelector('#input')
Multiple sessionsbrowser.newContext()

Summary

In this chapter, you learned how to:

  • Manage multiple pages (tabs) and handle new window pop-ups.
  • Share or isolate sessions using browser contexts.
  • Interact with iframes and nested frames.
  • Synchronize complex workflows using proper waiting techniques.

Handling multiple pages and frames effectively turns your Playwright automation into a true simulation of real-world browsing behavior, it’s essential for modern applications involving OAuth, payments, or embedded content.