ScreenshotNeo

BlogHow-to

How to Capture Screenshots of Authenticated Pages with Playwright

Reuse Playwright authentication state to capture protected pages, with runnable TypeScript, Python, and JavaScript examples plus fixes for common session issues.

By the ScreenshotNeo team4 October 20269 min read

To capture a page that requires sign-in, authenticate in a Playwright browser context, wait for a visible signal that login finished, save that context’s storage state, and load the saved state in the context that visits the protected route. Then call the page screenshot API. The state file can contain credentials, so keep it out of source control.

The examples below use TypeScript and Playwright Test, followed by standalone Python and JavaScript versions. Login URLs, selectors, and signed-in indicators are application-specific; replace the placeholders with those from your app.

1. Install Playwright and prepare an auth-state directory

For a TypeScript Playwright Test project:

npm init playwright@latest

Install the browser binaries if your project does not already have them:

npx playwright install

Add the state directory to .gitignore. Authentication state may include cookies and headers that can impersonate the account.

# .gitignore
playwright/.auth/

Create the directory before writing the state file:

mkdir -p playwright/.auth

2. Log in once and save the browser context state

Playwright recommends completing authentication in a setup step, then reusing the saved state in tests. Wait for the final redirect or a signed-in UI element before saving: cookies may be set across redirects, and saving too early can produce a file that looks valid but does not authenticate the next context.

Example setup file, tests/auth.setup.ts:

import { test as setup, expect } from '@playwright/test';
import path from 'node:path';

const authFile = path.join(process.cwd(), 'playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL ?? '');
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD ?? '');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Adapt this to a stable, app-specific post-login signal.
  await expect(page.getByRole('link', { name: 'Account' })).toBeVisible();

  await page.context().storageState({ path: authFile });
});

Provide credentials through environment variables or your CI secret store. Do not put real credentials in the test file. Replace the labels and button text with selectors that match your login form.

Configure the setup project and a dependent project in playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

The setup project runs before the dependent project in a normal Playwright Test run. UI mode does not run setup projects by default; run the setup project manually when needed. If the saved login expires, rerun setup to generate fresh state.

3. Visit the protected route and capture the screenshot

Because the Chromium project is configured with storageState, its pages start with the saved browser state. Check a destination-specific signed-in signal before saving the image so a redirect to the login screen is not mistaken for a successful capture.

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

 test('captures the signed-in account page', async ({ page }) => {
  await page.goto('https://example.com/account');
  await expect(page.getByRole('heading', { name: 'Your account' })).toBeVisible();
  await page.screenshot({ path: 'authenticated-account.png', fullPage: true });
});

Remove the leading space before test if copying the snippet into a formatter that flags it; it is valid JavaScript whitespace. Use fullPage: true when you need the full document rather than the visible viewport. The output path is relative to the process working directory.

4. Choose shared or per-worker authentication

A single saved account state is convenient when tests can run concurrently without conflicting through shared server-side changes. If tests modify shared data, create a separate account and state for each parallel worker. Browser-specific authentication is another reason to avoid assuming one shared state file will work for every project.

Situation Approach
Read-only tests or tests that safely share an account Authenticate in setup once and reuse the state.
Tests mutate the same server-side records Use separate accounts and state per worker to avoid interference.
Login state differs by browser Generate and validate state for the relevant browser project.
State should be removed after a run Store it in the test project’s output directory, which Playwright cleans before a run.

Consider the account’s server-side effects and whether the app binds sessions to a browser or device before choosing a strategy.

5. Know which browser state is saved

Playwright’s storageState() covers cookies and local storage. Current BrowserContext API documentation also describes options for IndexedDB, virtual WebAuthn credentials, and origin-private file-system state. Availability and option details depend on the installed Playwright version, so check the matching API documentation before relying on them.

If the app stores its authentication token in IndexedDB, include it when saving state using the option supported by your version, for example:

await page.context().storageState({
  path: 'playwright/.auth/user.json',
  indexedDB: true,
});

Session storage is different: it is not automatically persisted by storageState. If the app actually depends on session storage, save the relevant origin-specific values yourself and seed them with an initialization script before the app’s scripts run. Avoid this extra mechanism when the app does not use session storage for sign-in; custom restoration adds maintenance and security concerns.

For virtual WebAuthn credentials or OPFS state, consult the installed version’s BrowserContext API and verify support for your browser. The API documentation notes that OPFS is not supported in ephemeral WebKit contexts.

6. Standalone Python example

Install the Python package and browser binaries:

python -m pip install playwright
python -m playwright install chromium

This script logs in, waits for an app-specific signed-in marker, saves state, opens a fresh context with that state, verifies the protected page, and captures it:

import os
from pathlib import Path
from playwright.sync_api import sync_playwright, expect

AUTH_FILE = Path('playwright/.auth/user.json')
AUTH_FILE.parent.mkdir(parents=True, exist_ok=True)

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    login_context = browser.new_context()
    page = login_context.new_page()
    page.goto('https://example.com/login')
    page.get_by_label('Email').fill(os.environ['TEST_USER_EMAIL'])
    page.get_by_label('Password').fill(os.environ['TEST_USER_PASSWORD'])
    page.get_by_role('button', name='Sign in').click()
    expect(page.get_by_role('link', name='Account')).to_be_visible()
    login_context.storage_state(path=str(AUTH_FILE))
    login_context.close()

    context = browser.new_context(storage_state=str(AUTH_FILE))
    protected = context.new_page()
    protected.goto('https://example.com/account')
    expect(protected.get_by_role('heading', name='Your account')).to_be_visible()
    protected.screenshot(path='authenticated-account.png', full_page=True)
    browser.close()

7. Standalone JavaScript example

Install Playwright and its Chromium browser:

npm install playwright
npx playwright install chromium

Save as capture-authenticated.mjs and run with credentials in the environment:

import { chromium, expect } from 'playwright';
import { mkdir } from 'node:fs/promises';

const authFile = 'playwright/.auth/user.json';
await mkdir('playwright/.auth', { recursive: true });

const browser = await chromium.launch({ headless: true });
const loginContext = await browser.newContext();
const page = await loginContext.newPage();
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL ?? '');
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD ?? '');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('link', { name: 'Account' })).toBeVisible();
await loginContext.storageState({ path: authFile });
await loginContext.close();

const context = await browser.newContext({ storageState: authFile });
const protectedPage = await context.newPage();
await protectedPage.goto('https://example.com/account');
await expect(protectedPage.getByRole('heading', { name: 'Your account' })).toBeVisible();
await protectedPage.screenshot({ path: 'authenticated-account.png', fullPage: true });
await browser.close();

8. Screenshot options and practical capture choices

The essential call is page.screenshot(). For a viewport screenshot, use await page.screenshot({ path: 'shot.png' }). For a full-document image, pass fullPage: true. The Page screenshot API supports additional options; check the docs for your Playwright version when you need format, clipping, animation handling, or masking behavior.

  • Viewport versus full page: viewport captures are smaller and predictable; full-page captures may be tall and can expose more account data.
  • Wait for the right state: wait for a specific heading, table, or signed-in marker instead of using an arbitrary sleep when possible.
  • Dynamic content: if the view updates after the initial marker appears, wait for the relevant content or a stable app condition before capture.
  • Private content: review what the image contains before retaining it or sharing it in build artifacts.

9. Troubleshooting

Symptom Likely cause Fix
Protected route redirects to login State was saved before authentication completed, expired, or belongs to another app origin. Wait for a post-login signal before saving; rerun setup; confirm the destination origin matches the login session.
Cookies exist but the app still considers the user signed out The app stores an auth token in IndexedDB or sessionStorage instead of the saved cookies/local storage. Use the supported IndexedDB storage option if needed. For sessionStorage, implement an origin-specific initialization script.
Setup passes locally but fails in CI Credentials, MFA, redirects, or timing differ in the CI environment. Supply CI secrets, wait on a stable final URL or signed-in UI, and adapt the flow to the app’s actual MFA policy. Do not assume the example login selectors fit your site.
Tests interfere with one another Parallel tests mutate shared account data. Use distinct accounts and saved state per worker.
Auth setup did not run in UI mode Playwright UI mode does not run the setup project by default. Run the setup project manually to regenerate the state before dependent tests.
State file is missing The parent directory was not created or the setup project did not finish. Create the directory, verify setup completes, and check the configured file path relative to the project working directory.
Screenshot shows a partial or stale page Capture occurred before the target content rendered or updated. Wait for a destination-specific visible element or state change before calling screenshot.
Playwright rejects a state option The installed package version does not support that option. Check the BrowserContext API docs for the installed version and upgrade only if the project can safely adopt the newer version.

10. Reliability, performance, and cost

Saving state once avoids repeating an interactive login flow for every capture, but the state can expire or be invalidated by the application. Treat setup as a renewable prerequisite: regenerate state when login expires, and make setup fail clearly when the signed-in marker never appears. No saved-state method guarantees persistence across MFA challenges, server-side session revocation, or browser-specific policies; those depend on the application.

Full-page captures can take more time and produce larger files than viewport captures because more page content must be rendered and encoded. Capture only the region you need, wait on observable content rather than long fixed delays, and avoid unnecessary relogins. The browser and test execution cost depends on where you run Playwright; the workflow itself does not imply a particular hosting cost or performance figure.

Or skip the browser setup

If you need an image from a publicly reachable page and do not need your own authenticated session, ScreenshotNeo provides a screenshot API and MCP server. For authenticated pages, do not send private credentials to a service unless its supported authentication configuration and your security requirements permit it. The API base is documented at ScreenshotNeo docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status.
  • An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month with no card.

FAQ

Can I capture a page that requires a one-time MFA challenge?

Sometimes. Complete the challenge during setup and save state only after a verified signed-in signal. The application may require another challenge later or invalidate the session.

Can I commit the auth JSON file to make CI easier?

No. It can contain impersonation-capable cookies or headers. Keep it out of version control and provide credentials and generated state through appropriately restricted CI storage.

Does storageState save sessionStorage?

No. Playwright documents sessionStorage as requiring a separate manual persistence and initialization approach when an application relies on it.

Can multiple tests use the same saved state?

Yes, when their concurrent use of the account does not conflict with shared server-side data and the auth is not browser-specific. Otherwise, use separate worker accounts.

References