How to capture screenshots of authenticated pages in different browser contexts
Reuse Playwright authentication state to capture signed-in pages across isolated browser contexts, verify login, handle session storage, and protect credentials.
To capture an authenticated page reliably, sign in in a setup context, wait until login is confirmed, save the browser context’s authentication state, then create a separate context from that state and take the screenshot from a page inside it. Use a different state file and context for each user or role. Playwright storage state can preserve cookies, local storage, and—in supported versions—IndexedDB, but session storage needs separate handling.
This guide uses Playwright with Node.js. A browser context carries the session; the page screenshot captures the rendered result. Confirm the signed-in state before capturing so a redirect or loading screen does not produce a misleading screenshot. See the official Playwright authentication guide, BrowserContext API, and Page API.
1. Install Playwright and prepare private state storage
Install Playwright and its Chromium browser in your project:
npm init -y
npm install -D playwright
npx playwright install chromium
Create a private directory for authentication snapshots and ignore it in Git:
mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
Authentication state can contain cookies and headers that impersonate the account. Keep it out of source control, published artifacts, screenshots, and logs. Use a dedicated test account, and keep captured images free of customer data when possible.
2. Sign in once and save the context state
Use the application’s normal sign-in flow, then wait for a reliable post-login condition before saving. This example uses environment variables for the login URL and credentials; adapt the selectors and post-login URL to your application.
// save-auth.mjs
import { chromium } from 'playwright';
const loginUrl = process.env.LOGIN_URL;
const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
if (!loginUrl || !username || !password) {
throw new Error('Set LOGIN_URL, TEST_USERNAME, and TEST_PASSWORD');
}
const browser = await chromium.launch();
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(loginUrl, { 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 URL or a visible element unique to signed-in pages.
await page.waitForURL('**/dashboard', { timeout: 30_000 });
await page.getByRole('navigation').waitFor({ state: 'visible' });
await context.storageState({ path: 'playwright/.auth/test-user.json' });
} finally {
await browser.close();
}
Run it with your own login URL and test credentials:
LOGIN_URL='https://app.example.test/login' TEST_USERNAME='qa@example.test' TEST_PASSWORD='replace-me' node save-auth.mjs
Do not put real credentials directly in a committed script or shell history. Supply them through your CI secret store or another private environment mechanism. If the application’s authentication depends on IndexedDB, use the installed Playwright version’s documented IndexedDB option when saving state; support is version-dependent, so check the API documentation for your installed version.
3. Create an independent context and capture the page
Pass the saved state when creating a new context, then create the page inside that context. The following script verifies a signed-in marker and the target content before saving a full-page PNG:
// capture-authenticated.mjs
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const context = await browser.newContext({
storageState: 'playwright/.auth/test-user.json',
viewport: { width: 1440, height: 1000 },
});
const page = await context.newPage();
await page.goto('https://app.example.test/reports', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
// Replace with an element that only appears after successful authentication.
await page.getByRole('navigation').waitFor({ state: 'visible', timeout: 15_000 });
await page.getByRole('heading', { name: 'Reports' }).waitFor({ state: 'visible' });
await page.screenshot({ path: 'reports.png', fullPage: true });
await context.close();
} finally {
await browser.close();
}
Run it with node capture-authenticated.mjs. For a viewport-only image, omit fullPage or set it to false. A full-page capture can be taller than the viewport and may trigger lazy-loaded content; wait for the particular content your capture needs before taking it.
4. Capture separate users or roles without mixing sessions
A BrowserContext is an independent browser session. Create a separate context and state snapshot for each user, role, or session you need to test. Do not reuse a single context when the point of the test is to keep sessions isolated.
// capture-roles.mjs
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
for (const role of ['admin', 'member']) {
const context = await browser.newContext({
storageState: `playwright/.auth/${role}.json`,
viewport: { width: 1440, height: 1000 },
});
try {
const page = await context.newPage();
await page.goto('https://app.example.test/settings', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await page.getByRole('navigation').waitFor({ state: 'visible' });
await page.screenshot({ path: `settings-${role}.png`, fullPage: true });
} finally {
await context.close();
}
}
} finally {
await browser.close();
}
Each role needs its own valid state file, created through an appropriate login flow. Pages and popups belong to their parent context, so a popup opened by a page does not silently switch to another user session. Cookies added to a context apply to pages in that context.
5. Handle session storage and other authentication state
Do not assume cookies alone recreate a login. Applications may keep authentication material in local storage, IndexedDB, or session storage, and the correct mechanism depends on the application. Playwright’s standard storage-state workflow does not persist session storage across page loads. The authentication guide demonstrates restoring it with an initialization script.
One approach is to capture only the required session-storage entries after confirming login, then restore them for the intended origin before application scripts run:
// Capture after login in the setup flow. Keep this file private like auth state.
const sessionData = await page.evaluate(() => {
const values = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
values[key] = sessionStorage.getItem(key);
}
return values;
});
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('playwright/.auth/session.json', JSON.stringify(sessionData), { mode: 0o600 })
);
// Before navigation in the capture context, install an origin-scoped initializer.
import { readFile } from 'node:fs/promises';
const sessionDataToRestore = JSON.parse(
await readFile('playwright/.auth/session.json', 'utf8')
);
await context.addInitScript(({ expectedOrigin, values }) => {
if (location.origin !== expectedOrigin) return;
for (const [key, value] of Object.entries(values)) {
if (typeof value === 'string') sessionStorage.setItem(key, value);
}
}, { expectedOrigin: 'https://app.example.test', values: sessionDataToRestore });
Integrate the capture snippet after the authenticated setup page is verified and the restore snippet after creating the new context but before navigating to the protected page. Keep the origin check narrow and restore only values the application needs. Session storage may be tied to a tab or workflow, so confirm that restoring it matches the app’s behavior.
Playwright’s current BrowserContext documentation describes storage-state support for cookies, local storage, IndexedDB snapshots, origin private file system state, and virtual WebAuthn credentials. IndexedDB export is documented as added in v1.51, setStorageState in v1.59, and OPFS support in v1.63. These features are version-sensitive; check your installed Playwright version and its API docs before depending on them.
6. Make captures deterministic
- Verify identity and authorization: wait for a stable signed-in marker, then assert target-specific content. A screenshot shows rendered pixels; it does not prove that access was authorized.
- Wait for the right state: prefer a known URL or visible element over an arbitrary sleep. If the page hydrates asynchronously, wait for the element that represents the finished state.
- Control the viewport: use the same viewport for comparable captures. Set device scale factor if pixel density matters; keep it consistent between runs.
- Wait for lazy content: scroll or wait for the relevant images and sections to load before a full-page screenshot when needed.
- Avoid unnecessary cross-context reuse: use a fresh context when each capture must start from the same saved state. Changes made in one context should not be assumed to appear in another.
- Close resources: close each context and the browser, including on errors, to avoid leaking browser processes in repeated jobs.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the login page | State was saved before login completed, has expired, or does not include the app’s auth mechanism. | Re-run sign-in, wait for a signed-in URL or marker before saving, and check whether auth uses IndexedDB or session storage. |
| One role appears as another user | A context or state file was reused across roles. | Maintain separate state files and create one new context per role. Keep role names in output filenames. |
| Works in setup but not after restore | The application relies on session storage or another non-restored state. | Inspect the app’s auth mechanism; restore only required session-storage keys with an origin-scoped initialization script, or use the relevant supported storage-state option. |
| Navigation times out | The page never reached the chosen load condition, or the server is slow. | Use an appropriate navigation condition such as domcontentloaded, set a considered timeout, and separately wait for a meaningful signed-in element. |
| Screenshot is a loading screen or incomplete page | Capture happened before hydration, API content, or lazy images finished. | Wait for the specific content or image condition. Avoid relying on fixed delays unless the application has no observable readiness signal. |
| Storage-state option is rejected | The installed Playwright version lacks that option or API. | Check the installed version and current BrowserContext API documentation; update only if the project can adopt that version. |
| HTTP credentials seem ignored after changing them | Browsers may cache credentials per origin after successful authentication. | Use a fresh context for the changed credentials and verify against the intended origin. |
| Auth state appears in Git or logs | The private state directory was not ignored or the workflow printed sensitive contents. | Remove it from tracked files, rotate/revoke the affected test session if possible, and avoid logging state or publishing artifacts that contain it. |
8. Performance, reliability, and cost
Reusing saved state avoids repeating the interactive sign-in flow for every capture, but it does not make a session permanent: state can expire or be revoked. Refresh it through the normal login flow when the signed-in checks fail. A fresh context per role provides isolation; keeping one browser process open and creating contexts within it can avoid launching a browser for each role, while still separating sessions.
Use explicit waits for the content that matters. Broad network-idle waits can be unreliable on pages with ongoing requests, while arbitrary long sleeps slow the run and can still miss delayed content. Full-page screenshots can take longer and use more memory for very tall pages. Capture only the needed viewport or element when that satisfies the task.
Local Playwright captures have no per-screenshot ScreenshotNeo charge; their cost is the compute and maintenance of the machine or CI runner, browser installation, and time spent keeping the automation reliable. For hosted capture without managing browser setup, see the option below. ScreenshotNeo’s listed plans are 1,000 shots/month free with no card, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is on every plan.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request captures a public URL as PNG, JPEG, WebP, or PDF. Its API is documented at ScreenshotNeo docs.
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}`);
These examples capture the supplied URL; they do not transfer a Playwright storage-state file or authenticate to a private page. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot, and those cleanup steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page info, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I use one storage-state file for several pages?
Yes, if those pages should share the same user session and the state remains valid. Create the pages in the context initialized from that file.
Do browser contexts include new tabs and popups?
Yes. A popup belongs to the context of the page that opened it, so it uses that session.
Does a screenshot verify that a user is allowed to see the page?
No. It records the rendered page. Verify authorization through your application’s own checks and use only accounts and pages you are permitted to access.
Can Selenium do this too?
Selenium documents working with browser windows and tabs and supports screenshot capture, including element screenshots in its API. Choose based on your language binding, existing test stack, browser needs, and how your application stores authentication. See the Selenium windows and tabs documentation.


