ScreenshotNeo

BlogHow-to

How to capture a Playwright screenshot when a download button changes the page

Capture the page before a download changes it, or wait for Playwright’s download, navigation, or UI state before taking a post-click screenshot.

By the ScreenshotNeo team4 October 20267 min read

Choose the screenshot timing based on what the button does. To capture the page as it looks before the click, await page.screenshot() first. If the click starts an attachment download, register a wait for Playwright’s download event before clicking, then await and save the download. If it navigates or updates the page in place, wait for the destination or UI state you care about and take the screenshot afterward.

The button label alone does not tell you which behavior to expect. A download event means an attachment download started; it does not mean a page transition or an in-page update completed. Use the condition that matches the site’s actual behavior.

1. Capture the page before clicking

If you need the page exactly as it appeared before the action, save the screenshot before clicking. Awaiting the call ensures the screenshot operation has completed before the click can change the visible page.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();

try {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.getByRole('button', { name: 'Download' }).waitFor({ state: 'visible' });

  await page.screenshot({ path: 'before-click.png' });
  await page.getByRole('button', { name: 'Download' }).click();
} finally {
  await browser.close();
}

Replace the example URL and accessible button name with values from your page. If the page needs more time to render the content you want in the image, wait for a specific locator or application state before taking the screenshot. A screenshot taken after the click cannot reconstruct the old visible state.

2. Wait for an attachment download

When clicking starts a file download, start waiting for the event before clicking. This ordering matters because a fast download may begin immediately; attaching the wait afterward can miss it.

import { chromium } from 'playwright';
import { join } from 'node:path';

const browser = await chromium.launch();
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();

try {
  await page.goto('https://example.com');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Download' }).click();
  const download = await downloadPromise;

  const outputPath = join(process.cwd(), download.suggestedFilename());
  await download.saveAs(outputPath);
  console.log(`Saved download to ${outputPath}`);

  // Capture the page after the download starts, if that is the state you need.
  await page.screenshot({ path: 'after-download-started.png' });
} finally {
  await context.close();
  await browser.close();
}

download.suggestedFilename() provides the suggested name. Choose a destination you control if the filename may contain unexpected characters or collide with an existing file. Save files that need to persist: downloads associated with a browser context are deleted when that context closes.

If you want the pre-click view as well as the post-click view, take both screenshots explicitly. The event confirms that a download started; it does not determine what the page looks like afterward.

3. Wait for navigation or an in-page update

A click may navigate to another route, replace content in the current document, or do both. For navigation, wait for the expected URL or destination. For an in-page change, wait for a locator or state that represents the result you need. Then take the screenshot.

const destinationPromise = page.waitForURL('**/download-complete');
await page.getByRole('button', { name: 'Download' }).click();
await destinationPromise;
await page.screenshot({ path: 'destination.png' });

Use the actual URL pattern for the destination. If the click opens a new tab instead, wait for the context’s page event and capture that page after it reaches the expected state.

In-page update example

await page.getByRole('button', { name: 'Download' }).click();
await page.getByRole('status').filter({ hasText: 'Download ready' }).waitFor();
await page.screenshot({ path: 'download-ready.png' });

Replace the status locator and text with an element that reflects the real result. Waiting for a fixed delay can be useful when a short delay is part of the page’s behavior, but a meaningful locator or state is usually more reliable than guessing a duration.

4. Choose the right screenshot capture method

Need Playwright approach Considerations
Current visible viewport await page.screenshot({ path: 'page.png' }) Captures the page’s current visible state.
Entire scrollable page await page.screenshot({ path: 'full.png', fullPage: true }) Captures beyond the viewport; lazy-loaded content may need to be triggered before capture.
One element await page.locator('#result').screenshot({ path: 'result.png' }) Wait for the target element to be visible and stable enough for the capture.
Image bytes in memory const bytes = await page.screenshot() Returns a buffer rather than requiring a path.
Visual regression assertion await expect(page).toHaveScreenshot() Playwright Test assertion; it waits for consecutive screenshots to stabilize before comparison.

For any of these, timing still matters: take the screenshot before the click for the old page state, or wait for the intended post-click state before capturing.

5. Runnable complete example: download and both page states

This example saves the page before the click, waits for the attachment download, saves that file, and captures the visible page after the download begins. It assumes the button starts an attachment download and that the page remains open.

import { chromium } from 'playwright';
import { join } from 'node:path';

const targetUrl = 'https://example.com';
const browser = await chromium.launch();
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();

try {
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
  const downloadButton = page.getByRole('button', { name: 'Download' });
  await downloadButton.waitFor({ state: 'visible' });

  await page.screenshot({ path: 'before-download.png', fullPage: true });

  const downloadPromise = page.waitForEvent('download');
  await downloadButton.click();
  const download = await downloadPromise;

  const filePath = join(process.cwd(), download.suggestedFilename());
  await download.saveAs(filePath);

  await page.screenshot({ path: 'after-download-started.png' });
  console.log(`Download saved to ${filePath}`);
} finally {
  await context.close();
  await browser.close();
}

For a button that navigates or updates the current document, replace the download wait and file save with the matching navigation or UI-state wait in section 3. Do not wait for a download event unless the site actually starts an attachment download.

6. Common errors and fixes

Symptom Likely cause Fix
The screenshot shows the changed page, not the original. The click happened before the screenshot completed. Await page.screenshot() before clicking.
The download wait times out. The button navigates or updates the page instead of starting an attachment, or the click did not trigger the intended action. Check the site’s behavior and wait for the expected URL or UI state if that is what changes.
The download event is missed. The event wait was registered after the click. Create the waitForEvent('download') promise before clicking, then await it afterward.
The saved file disappears. The browser context closed before the file was saved to a persistent path. Call download.saveAs() before closing the context.
The screenshot is blank or missing content. The page or target content was not ready when the screenshot ran. Wait for the relevant locator or app state before capture; verify that navigation completed.
A fixed timeout works intermittently. Load or rendering time varies. Prefer a condition tied to the destination or rendered UI over an arbitrary delay.
Visual test output differs between runs. The page may not have reached a stable visual state, or dynamic content may change. Wait for the relevant state and use Playwright Test’s screenshot assertion when doing visual regression.

7. Reliability, speed, and file handling

  • Subscribe before acting. Set up the download or navigation wait before clicking to avoid missing a fast event.
  • Wait for evidence of readiness. A visible button is not proof that the post-click result has rendered. Wait for the page-specific destination or state that matters.
  • Keep context lifetime in mind. Save a required download before closing its browser context.
  • Capture only what you need. A viewport screenshot is usually narrower and simpler than a full-page capture. Full-page screenshots can include content that appears below the fold.
  • Use deterministic output paths. Suggested filenames can vary or collide; choose and manage paths appropriate for your workflow.
  • Do not assume a download changes the page. A file can download while the page remains visually unchanged. Capture before and after only when both states matter.

Playwright’s screenshot API provides path, full-page, buffer, and element capture approaches. For visual assertions, toHaveScreenshot() is available with the Playwright test runner and waits for consecutive screenshots to match before comparison. No timing or performance benchmark is implied here; the work required depends on the page, content, and capture scope.

8. FAQ

Can I screenshot the page and still download the file?

Yes. Take a screenshot before clicking, then handle the download event. You can also take another screenshot after the download starts if that state is useful.

Should I wait for a download event or navigation?

Wait for the event that matches the site’s behavior. An attachment uses the download event; a route change uses a destination condition; an in-page update uses an application-specific state.

Can I use a screenshot assertion for a one-off image?

page.screenshot() saves or returns an image directly. toHaveScreenshot() is a Playwright Test assertion for screenshot comparison.

Why save the download before closing the browser?

Files associated with a browser context are removed when that context closes. Save any file you need to keep before teardown.

Or skip the browser setup

If you need a screenshot of a public page without managing a Playwright browser, ScreenshotNeo returns an image or PDF from one API request. Its API documentation describes the request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Use your own API key and target URL. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.