ScreenshotNeo

BlogHow-to

How to Take Screenshots of a Logged-In Web App with a Rotating Session Cookie

Use Playwright to keep an authenticated browser context, handle cookie rotation, verify the signed-in page, and capture a reliable screenshot.

By the ScreenshotNeo team4 October 20269 min read

Use one Playwright BrowserContext for login, navigation, and capture. The context owns the browser’s cookie store, so normal responses can replace a rotating session cookie and subsequent requests use the updated value. Wait for a visible signed-in condition, then capture the page. If you reuse the session in another run, save and load the context’s storage state, verify it is still valid, and log in again when it has expired.

This approach preserves the real browser session. Do not hard-code an old cookie into each request: rotation can make that value stale, and a saved state file is only a snapshot. Playwright’s authentication guide recommends keeping authentication files out of source control because they may contain sensitive cookies and headers.

1. Install Playwright and choose a login flow

Use the application’s normal login UI when the screenshot needs to reflect the full user journey. If the app offers a supported authentication API, you can use it to establish a session more quickly, provided it creates all browser state the app needs. Playwright documents both UI-based and API-assisted authentication.

npm init -y
npm install -D playwright
npx playwright install chromium

The example below assumes environment variables for the app URL and a dedicated test account. Keep credentials outside the source file.

APP_URL=https://app.example.com
APP_USER=qa@example.com
APP_PASSWORD=replace-with-test-secret

For a real project, provide these through your CI secret store or shell environment. Use a minimally privileged test account and a non-production environment when possible.

2. Log in, wait for success, and capture

Save this as capture.mjs. Replace the login selectors and success marker with those used by your app. The script waits for an observable result instead of assuming a click means authentication finished. This matters when redirects set or rotate cookies across several responses.

import { chromium } from 'playwright';

const baseURL = process.env.APP_URL;
const username = process.env.APP_USER;
const password = process.env.APP_PASSWORD;

if (!baseURL || !username || !password) {
  throw new Error('Set APP_URL, APP_USER, and APP_PASSWORD');
}

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

try {
  await page.goto(new URL('/login', baseURL).toString(), {
    waitUntil: 'domcontentloaded',
  });
  await page.getByLabel('Email').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Prefer a stable authenticated marker or the final route in your app.
  await page.getByTestId('signed-in-user').waitFor({ state: 'visible' });

  await page.goto(new URL('/reports', baseURL).toString(), {
    waitUntil: 'domcontentloaded',
  });
  await page.getByTestId('signed-in-user').waitFor({ state: 'visible' });

  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await context.close();
  await browser.close();
}

Replace getByTestId('signed-in-user') with a dependable marker such as an account menu, user name, or authenticated-page heading. A final URL check can also help, but a visible marker confirms the app rendered signed-in content. The default screenshot is the viewport; fullPage: true captures the full scrollable page. See Playwright’s Page screenshot options for image options and masking.

3. Reuse storage state when another run needs the session

After the authenticated marker appears, save the context state. It includes cookies and local storage. On a later run, create the context from the file, navigate to the target route, and check the signed-in marker again. If the app rotated or expired the session after the snapshot, repeat login and save a fresh state.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();

try {
  await page.goto('https://app.example.com/reports', {
    waitUntil: 'domcontentloaded',
  });
  await page.getByTestId('signed-in-user').waitFor({ state: 'visible' });
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await context.close();
  await browser.close();
}

Create the file after login with await context.storageState({ path: 'playwright/.auth/user.json' }). Add playwright/.auth to .gitignore, restrict access to the file, and avoid uploading it as a general build artifact. Treat it like a password: someone who can use it may be able to impersonate the test account.

Cookies belong to the browser context. Keep using the same context through login and page navigation; when a response sets a replacement cookie, the browser updates its cookie store and uses the current cookie on later matching requests. Do not extract a cookie once and repeatedly send that original value yourself.

Playwright also documents that API requests associated with a BrowserContext share its cookie storage, and response Set-Cookie headers update browser cookies. For example:

const response = await context.request.get('https://app.example.com/api/session-refresh');
console.log('refresh status:', response.status());
// Any Set-Cookie response is applied to this context's cookie store.

Use only the app’s supported API endpoints and authorized test account. The app determines cookie domain, path, expiry, and rotation policy. Cookie APIs require correct domain and path matching; in ordinary flows, letting login establish cookies is safer than manually seeding them. See the official BrowserContext documentation and API testing guide.

5. Choose viewport or full-page capture

Capture Use it when Trade-off
Viewport (default) You need evidence of the currently visible screen or a stable test image. Content below the fold is omitted.
fullPage: true You need the whole scrollable document. The output can be tall and may include more sensitive content; lazy-loaded sections may need scrolling or app-specific waits first.

For sensitive account details, use Playwright’s screenshot masking option, for example mask: [page.locator('[data-sensitive]')]. Confirm the selector matches the content you intend to hide. Keep the original image access-controlled as well.

6. Handle special cases

  • Session storage: standard storage-state reuse does not generally include session storage. If your app depends on it, follow Playwright’s documented save-and-restore approach with an initialization script, scoped to the correct origin.
  • IndexedDB or passkeys: authentication state may involve browser storage beyond cookies and local storage. Check the current Playwright authentication guide for support and your installed version’s API.
  • Rotation during a long flow: continue in the same context so browser-managed updates apply. If the app returns to login or rejects a request, restart the authorized login flow rather than fabricating cookie values.
  • Domain and path: cookies only accompany requests matching their scope. A cookie for one subdomain or path may not authenticate another route.
  • Redirects: a login button click may begin several redirects. Wait for the final route or signed-in UI before saving state or taking the shot.
  • Lazy content: full-page capture does not guarantee every application-specific lazy component has loaded. Scroll relevant areas or wait for a content marker before capture.

7. cURL, Python, and Node.js alternatives

A raw HTTP client can preserve cookies between requests using a cookie jar, but it does not render a web page and cannot take a browser screenshot by itself. Use these patterns for authorized HTTP authentication or API checks; use Playwright for the rendered screenshot.

curl -L -c cookies.txt -b cookies.txt \
  -d 'email=qa%40example.com&password=YOUR_TEST_PASSWORD' \
  https://app.example.com/login

curl -L -c cookies.txt -b cookies.txt \
  https://app.example.com/api/session-refresh

Adapt the form fields and CSRF handling to the app. The -c option writes cookies received from responses; -b sends the jar on the next request. Protect and delete cookies.txt like an authentication secret. This does not execute JavaScript or produce a screenshot.

Python requests session

import os
import requests

base = os.environ['APP_URL'].rstrip('/')
s = requests.Session()

login = s.post(
    f'{base}/login',
    data={
        'email': os.environ['APP_USER'],
        'password': os.environ['APP_PASSWORD'],
    },
    timeout=30,
    allow_redirects=True,
)
login.raise_for_status()

refresh = s.get(f'{base}/api/session-refresh', timeout=30)
refresh.raise_for_status()
page = s.get(f'{base}/reports', timeout=30)
page.raise_for_status()
print('cookies currently held:', list(s.cookies.keys()))

requests.Session keeps cookies received from responses and sends updated values on later matching requests. Adapt CSRF tokens, login fields, and success checks to your app. This retrieves HTTP content; it is not a browser renderer and does not create a screenshot.

Node’s built-in fetch does not provide a browser context or automatically maintain a browser-style cookie jar across calls. For HTTP-only checks, use a cookie-jar library or Playwright’s context-associated request. For screenshots, the runnable Playwright examples above are the appropriate Node.js method.

import { chromium } from 'playwright';

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

try {
  await page.goto('https://app.example.com/login');
  // Complete the app's login UI, then wait for its signed-in marker.
  await page.getByTestId('signed-in-user').waitFor({ state: 'visible' });

  const response = await context.request.get(
    'https://app.example.com/api/session-refresh'
  );
  if (!response.ok()) throw new Error(`Refresh failed: ${response.status()}`);

  await page.goto('https://app.example.com/reports');
  await page.getByTestId('signed-in-user').waitFor({ state: 'visible' });
  await page.screenshot({ path: 'page.png' });
} finally {
  await context.close();
  await browser.close();
}

8. Troubleshooting

Symptom Likely cause Fix
Screenshot shows login page State expired, cookie scope mismatch, or login did not finish. Wait for a signed-in marker after login; inspect final URL; authenticate again and save fresh state.
Works in the first run but fails on reuse Saved storage state is stale or the app revoked/rotated the session. Treat state as a snapshot. Run login again when the marker is absent, then overwrite the private state file.
API call is unauthorized but page is signed in The endpoint may need CSRF headers, a different origin, or state established by another app step. Use the app’s supported flow, preserve the same context, and inspect the response status and app requirements.
Cookie appears missing Wrong domain or path, Secure cookie over HTTP, or inspecting a different context. Use the exact context and HTTPS origin; check cookie scope through the context cookie API without logging values.
Wait times out Selector does not match, the app is still loading, or login failed. Use a stable test id or accessible locator; capture diagnostics such as final URL and console errors; verify credentials and test account status.
Image contains private data The authenticated page exposes account-specific content. Mask sensitive locators, use a minimal test account, and restrict access to screenshot artifacts.
Screenshot is blank or incomplete Capture occurred before app rendering, fonts, or lazy content completed. Wait for a meaningful content locator and, where needed, scroll the relevant content into view before full-page capture.

9. Performance, reliability, and cost

Browser startup and application rendering usually dominate this workflow; reusing a browser process can reduce repeated startup overhead, while keeping contexts separate prevents cookies from leaking between test identities. Reuse one context for a single authenticated flow and create a fresh context per independent account or test. Avoid fixed long sleeps when a specific locator or URL can signal readiness.

Reliability depends on the application’s session policy, not on the storage-state file. Cookie rotation, revocation, MFA, and short expiry can require a fresh login. Make the capture job detect an unauthenticated page and fail clearly rather than silently storing a login screenshot as the expected result. Playwright itself has no per-screenshot API fee; account for browser compute, CI time, and secure storage for images and auth state.

Or skip the browser setup

ScreenshotNeo takes a screenshot with one request and supports authenticated requests through custom headers and cookies. For a public page, this is the basic call; for a logged-in page, provide the required current cookie through the documented cookie option, and refresh it through your authorized login process when it rotates. A saved old cookie is not a durable session.

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

See the ScreenshotNeo API documentation for request options and authentication parameters. 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, and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

No. It saves a point-in-time browser state. The app controls cookie expiry and revocation, so verify the session each time and refresh the saved state after a successful login.

Can I use the same state file for multiple tests?

You can, but concurrent tests that change the same account’s session may invalidate each other. Use separate test accounts or authenticate independently when the app’s rotation policy makes shared state unreliable.

Can an HTTP client take the screenshot?

No. cURL and Python requests can maintain cookies for HTTP requests, but they do not render the application as a browser. Use Playwright for a screenshot of the rendered page.

Does full-page capture include content behind dialogs?

It captures the document’s scrollable page; it does not automatically dismiss app dialogs or prove hidden content was rendered. Handle dialogs and wait for the state the screenshot is meant to document.

Sources