ScreenshotNeo

BlogHow-to

How to Take Screenshots of a Multi-Step Page Flow with Playwright

Capture each meaningful UI state in a multi-step flow with Playwright. Learn how to synchronize screenshots, save artifacts, and compare visual baselines.

By the ScreenshotNeo team4 October 20268 min read

To capture a multi-step page flow with Playwright, take a screenshot after each meaningful state transition: open the starting page, perform one action, wait for evidence that the next state is ready, then save an ordered image. Repeat for each step. A full-page screenshot captures the scrollable document at the current state; it does not advance the flow. For visual regression, use Playwright Test’s toHaveScreenshot() assertion instead of saving unasserted images.

1. Set up a Playwright Test project

The following example uses TypeScript and Playwright Test. Install the test package and browser binaries in a project that already has Node.js and npm:

npm install --save-dev @playwright/test
npx playwright install

Create tests/checkout-flow.spec.ts. The paths, button names, and headings below are illustrative; replace them with the routes and accessible names used by your application. The example assumes your local app is available at http://localhost:3000.

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

// Replace with your application's local URL.
const baseURL = 'http://localhost:3000';

test('captures the checkout flow states', async ({ page }) => {
  await page.goto(`${baseURL}/cart`);
  await expect(page.getByRole('heading', { name: 'Your cart' })).toBeVisible();
  await page.screenshot({ path: 'artifacts/01-cart.png' });

  await page.getByRole('button', { name: 'Continue to delivery' }).click();
  await expect(page).toHaveURL(/\/delivery/);
  await expect(page.getByRole('heading', { name: 'Delivery' })).toBeVisible();
  await page.screenshot({ path: 'artifacts/02-delivery.png' });

  await page.getByRole('button', { name: 'Continue to payment' }).click();
  await expect(page).toHaveURL(/\/payment/);
  await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
  await page.screenshot({ path: 'artifacts/03-payment.png' });
});

Run it with npx playwright test tests/checkout-flow.spec.ts. This pattern follows documented Playwright APIs; it is not a tested run against a real checkout. The output directory must exist before writing screenshots, so create it in advance or configure your project to create it as part of the run.

2. Capture each state at the right moment

A screenshot records what the page currently renders. Treat each desired image as a checkpoint in the flow, and put it after an assertion that establishes the state you want to document or inspect.

  1. Open the initial route. Use page.goto() and wait for a meaningful signal, such as the page’s heading becoming visible.
  2. Capture the initial state. Use page.screenshot({ path: 'artifacts/01-start.png' }).
  3. Perform one user action. Prefer a locator action such as getByRole(...).click() so the step describes what a user does.
  4. Wait for the resulting state. Assert the expected URL, heading, dialog, or other state-specific content.
  5. Capture and continue. Save the image with an ordered, descriptive name before performing the next action.

Locator actions auto-wait for actionability, and Playwright’s assertions retry until the condition passes or times out. Choose locators that make the expected state clear to a reader. The Playwright locator guide describes locator behavior and recommendations.

3. Choose viewport, full-page, or element screenshots

  • Viewport: await page.screenshot({ path: 'artifacts/step.png' }) saves the visible viewport by default. Use this when the relevant UI fits on screen or when matching what a user sees at a particular viewport size.
  • Full page: await page.screenshot({ path: 'artifacts/step-full.png', fullPage: true }) captures the full scrollable document at the current flow state. It can make a long page easier to review, but it does not perform any subsequent flow steps.
  • Element: await page.locator('header').screenshot({ path: 'artifacts/header.png' }) captures one locator’s bounding box. Use a stable selector for the component you need to inspect.

These capture shapes answer different questions. A viewport image is useful for a state-by-state user journey; a full-page image adds page length for that same state; an element image narrows review to a component.

4. Save images as files, buffers, or report attachments

When you pass path, Playwright writes the screenshot to that file. Without a path, page.screenshot() returns a buffer, which you can attach to a named test step:

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

test('attaches a screenshot to the delivery step', async ({ page }) => {
  await page.goto('http://localhost:3000/cart');
  await expect(page.getByRole('heading', { name: 'Your cart' })).toBeVisible();

  await test.step('continue to delivery', async (step) => {
    await page.getByRole('button', { name: 'Continue to delivery' }).click();
    await expect(page.getByRole('heading', { name: 'Delivery' })).toBeVisible();
    const screenshot = await page.screenshot();
    await step.attach('delivery-state', {
      body: screenshot,
      contentType: 'image/png',
    });
  });
});

Attachments are useful when the image belongs with a diagnostic action or assertion in the Playwright Test report. The TestStepInfo API documents step attachments.

5. Use screenshots for visual regression

If the goal is to detect visual changes, use Playwright Test’s toHaveScreenshot(). It captures repeatedly until consecutive screenshots match, then compares the result with the approved reference image. The first run creates a reference; later runs compare against it. Review baseline changes before accepting them.

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

test('keeps checkout flow visuals stable', async ({ page }) => {
  await page.goto('http://localhost:3000/cart');
  await expect(page.getByRole('heading', { name: 'Your cart' })).toBeVisible();
  await expect(page).toHaveScreenshot('01-cart.png');

  await page.getByRole('button', { name: 'Continue to delivery' }).click();
  await expect(page.getByRole('heading', { name: 'Delivery' })).toBeVisible();
  await expect(page).toHaveScreenshot('02-delivery.png');
});

This matcher requires the Playwright Test runner. Keep reference images under version control and generate and compare them in consistent conditions: operating system, browser version, browser settings, hardware, power source, and headless mode can affect rendering. Dynamic content can make comparisons unstable; where appropriate, use screenshot options such as masks or a custom stylesheet to control it. See the official visual comparisons guide and PageAssertions API.

6. Synchronize on state, not elapsed time

Use an observable application signal to decide when a screenshot is ready. A visible heading, expected URL, dialog, or state-specific element gives the test a reason to proceed. For URL changes, assert with toHaveURL() or use page.waitForURL() where that better fits the flow.

Avoid using page.waitForTimeout() as routine production synchronization: a fixed delay can be too short on a slow run and unnecessarily long on a fast one. Playwright also discourages networkidle as a test readiness condition, since a page can be visually ready while background traffic continues. The Page API marks waitForNavigation() as inherently racy and recommends waitForURL() for URL changes. Consult the current Page API for your installed Playwright version.

7. Handle common edge cases

  • Single-page app transition: A route may change without a full document navigation. Assert on the resulting URL or on content that proves the new view is ready.
  • Validation errors: A step may stay on the same route when required input is missing. Assert the validation message before capturing that state, and provide valid test data when the intended screenshot is the next step.
  • Long pages: Choose fullPage: true if the whole document matters. If the purpose is to show a user-visible checkpoint, a viewport screenshot may communicate the state more clearly.
  • Dynamic content: Timestamps, rotating content, and randomized values can change between runs. For regression comparisons, mask or style unstable regions with supported screenshot options, or use stable fixture data.
  • State isolation: Each test should reach its own starting state. Do not rely on a previous test’s page state or on a flow having run in a particular order.
  • Authentication: Start with a controlled signed-in state using your existing test setup. Avoid embedding real credentials in test source or screenshot artifacts.
  • Screenshot timing: A click completing only confirms that the action ran; assert the next state before taking the image so that transitions and loading states are intentional.

8. Troubleshooting

Symptom Likely cause Fix
Screenshot shows the previous step The test captured immediately after the click, before the new state appeared. Assert the expected URL or state-specific content before capturing.
Locator or click times out The locator does not match the current page, or the element is hidden, disabled, or obstructed. Check the accessible name and current state; use a locator matching the intended control and wait for the relevant state.
Navigation wait hangs or is flaky The flow changes client-side, or the test uses a racy navigation wait. Assert the URL with toHaveURL() or waitForURL(); for client-side changes, assert the resulting content.
Screenshot file is missing The output directory does not exist or the relative path resolves somewhere unexpected. Create the directory and check the test process’s working directory and configured output paths.
Visual comparison fails only on another machine Browser or operating-system rendering differs, or the environment is inconsistent. Generate and compare baselines in the same browser and operating-system environment and review any baseline update.
Visual diff changes on every run Dynamic content or animation affects the captured pixels. Use stable test data and supported masking or stylesheet options for the varying region.
Full-page shot is unexpectedly tall fullPage: true includes the entire scrollable document. Use the default viewport capture or capture a specific element if that is the intended scope.

9. Performance, reliability, and cost

Each screenshot adds image capture and file or attachment handling to a test run. Capture only the states needed for review or regression coverage, and use ordered filenames so artifacts remain easy to map to actions. Full-page images cover more content than viewport images and can produce larger artifacts.

Reliable flow captures depend on deterministic starting state, meaningful readiness assertions, stable test data, and a consistent rendering environment. A fixed sleep may make a test slower without making it reliable. Keep visual baselines reviewed and aligned with the browser environment used for comparison. Playwright is a software library; this workflow has no screenshot-service charge, though browser execution and artifact storage consume your own project resources.

10. Or skip the browser setup

If you need a screenshot of a URL without scripting a browser flow, ScreenshotNeo returns a screenshot or PDF from one GET request. Its API does not advance through interactive steps like the Playwright example; use it for pages you can capture directly by URL.

See the ScreenshotNeo API documentation. Replace the target URL and use your API key:

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}`);

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its 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.

FAQ

Does one full-page screenshot capture every step in a flow?

No. It captures the full scrollable document at one point in the flow. Perform the actions and capture each desired state separately.

Should I save screenshots or use toHaveScreenshot()?

Save screenshots when you need standalone artifacts for documentation or debugging. Use toHaveScreenshot() when a test should compare a state with an approved visual baseline.

Can I use toHaveScreenshot() with Playwright’s library-only API?

The matcher is provided by Playwright Test, so use the Playwright Test runner for that assertion.

Why do screenshot baselines differ by machine?

Browser rendering can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Keep baseline generation and comparison environments consistent.

For current option syntax, check the official Playwright Page API and screenshots guide for the version in your project.