This episode covers advanced browser scenarios: handling frames, popups, and multiple tabs, drag and drop along with keyboard and mouse actions, automating file upload and download, as well as network interception and request mocking to control responses.

End-to-end tests that only click buttons and fill forms will quickly hit their limits. Real applications use iframes, open popups, involve drag and drop, file uploads, and communicate with servers that are sometimes unreliable. This episode 8 covers the advanced actions that unlock these scenarios.
These capabilities distinguish a test suite that merely runs from a suite that truly tests the application thoroughly. You'll learn to control elements inside frames, handle new windows, simulate complex input, automate files, and control network responses as if you were a proxy between the application and its server.
Elements inside an iframe cannot be accessed directly from page. Playwright provides page.frameLocator(), which returns a locator that operates inside a frame:
const frame = page.frameLocator('#payment-iframe');
await frame.getByLabel('Card Number').fill('4111111111111111');
await frame.getByRole('button', { name: 'Pay' }).click();page.frameLocator('#payment-iframe') returns a locator scoped to a specific frame. To handle frames you don't know in advance, use page.frames() to list all existing frames and pick as needed.
A popup is a new page opened from the parent page. Don't wait directly with page.waitForEvent after the action — set up the listener first to avoid a race condition:
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open Policy' }).click();
const popup = await popupPromise;
await popup.getByRole('heading').waitFor();page.waitForEvent('popup') returns a promise that resolves when the popup opens. Once you have the popup, actions can be directed to that new page instance, completely separate from the parent page.
A context allows opening several tabs and switching between them. page.bringToFront() makes a specific tab active:
const page2 = await context.newPage();
await page2.goto('https://example.com');
await page2.bringToFront();Using context.newPage() creates a new tab in the same context — these tabs share session storage and cookies. It's great for testing flows that involve several tabs at once.
There are two ways to drag and drop: via a dedicated method or through manual mouse actions:
await page.getByRole('listitem', { name: 'Item 1' }).dragTo(
page.getByRole('listitem', { name: 'Target' })
);dragTo() handles the mouse down, move, and up sequence automatically. If the application needs finer control, use page.mouse manually.
For special scenarios, keyboard and mouse actions can be fully controlled:
await page.getByRole('textbox').press('Control+a');
await page.keyboard.type('new text');
await page.mouse.move(100, 200);
await page.mouse.down();
await page.mouse.move(300, 400);
await page.mouse.up();page.keyboard.type('new text') types at the default speed, while page.mouse gives you pixel-coordinate control for complex manual drags.
Uploads can be done with setInputFiles on a file input, or with buffer payloads for scenarios that need flexibility:
await page.getByLabel('Upload Document').setInputFiles('report.pdf');
await expect(page.getByText('report.pdf')).toBeVisible();setInputFiles('report.pdf') accepts a local path. Playwright also supports buffer objects to create files directly from memory — useful when test data is generated dynamically.
To catch a download, set up the event listener before triggering the download:
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download Report' }).click();
const download = await downloadPromise;
await download.saveAs('./artifacts/report.pdf');download.saveAs('./artifacts/report.pdf') saves the downloaded file to the specified location. You can check download.suggestedFilename() to validate the file name.
With page.route, you can redirect requests to mocks, block certain requests, or modify them:
await page.route('**/api/products/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mock Product' }]),
});
});page.route('**/api/products/**', handler) intercepts all requests matching the URL pattern. Inside the handler, route.fulfill(...) returns a fabricated response without ever contacting the server.
To speed up tests, block requests for assets that aren't relevant, like fonts or analytics:
await page.route('**/analytics.js', (route) => route.abort());
await page.route('**/*.woff2', (route) => route.abort());route.abort() cancels the request so the browser doesn't download that resource. This reduces network noise and speeds up test execution on applications heavy with tracking scripts.
npx playwright test tests/advanced-actions.spec.tsThe command npx playwright test tests/advanced-actions.spec.ts runs the tests that use the techniques above. When frames, popups, and network mocking are combined, the trace viewer is your best friend for verifying the order of actions.
Episode 8 expanded the reach of your tests to scenarios that used to feel hard: elements inside iframes and popups, several tabs at once, precise drag and drop and keyboard/mouse actions, file upload and download, plus network interception and request mocking to control server responses.
Key takeaways:
frameLocator targets elements inside iframes without leaving the locator API.dragTo, page.keyboard, and page.mouse serve different levels of precision.setInputFiles and waitForEvent('download') automate file exchanges.page.route enables mocking, modifying, and blocking requests.In the next episode we'll discuss data-driven and parameterized testing — parameterized tests with test.each, external data sources like CSV and JSON, running the same scenario with many datasets, and best practices for repeatable coverage.