Use Playwright’s storageState for portable login reuse, and a persistent browser profile only when the browser itself must survive a restart. First identify where your application stores authentication data—cookies, localStorage, IndexedDB, origin-private data, sessionStorage, or WebAuthn credentials. Save only the stores you actually use, keep the resulting files secret, and automate MFA with a dedicated test account, virtual WebAuthn credentials, or tightly controlled OTP fixtures. Never disable production MFA or copy production credentials into tests.
Choose the persistence boundary first
“Stay logged in” can mean two different things. A Playwright storageState snapshot lets a new, isolated browser context start authenticated. A persistent profile writes the browser profile to disk so the browser can reopen with its cookies and other profile data after a restart.
| Approach | Survives browser restart | Portable to workers or another machine | Best fit | Main risk |
|---|---|---|---|---|
| In-memory context | No; it ends when the browser closes | No | One short-lived test or exploratory run | Every run must authenticate again |
storageState |
The snapshot can be reused after a restart | Yes, when copied securely | Parallel tests, CI, and repeatable suites | The file can contain cookies, headers, tokens, or private WebAuthn keys |
| Persistent profile | Yes | Usually no; profile locking and machine-specific data complicate sharing | Desktop-like workflows that require the same browser profile | State leakage between tests and profile corruption or locking |
Authentication is distributed. A site may use an HTTP cookie for the session, a bearer token in localStorage, an IndexedDB record, origin-private file-system data, or a passkey. Observe the post-login context and capture the stores your application actually uses rather than assuming cookies are sufficient.
Create a reusable Playwright authentication state
1. Log in once in a setup project
Keep authentication outside individual tests. The setup project below writes one state file for a test identity. Create the directory first and keep it out of source control.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page, context }) => {
await page.goto('https://app.example.test/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Include IndexedDB when this application stores its token there.
await context.storageState({ path: authFile, indexedDB: true });
});
The indexedDB option is useful only when your application depends on IndexedDB. If the site uses only cookies and localStorage, a normal storageState({ path }) snapshot is smaller and easier to reason about. Register any passkey through the normal enrollment flow in this isolated test account before taking the snapshot.
2. Make tests consume the snapshot
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'setup',
testMatch: /.*.setup.js/
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup']
}
]
});
import { test, expect } from '@playwright/test';
test('opens an authenticated page', async ({ page }) => {
await page.goto('https://app.example.test/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
Use a separate state file for each identity. If parallel workers mutate the same account, generate one state per worker and assign separate test data. This prevents one test from revoking, refreshing, or changing another test’s session.
3. Protect the state file
Authentication state is equivalent to a credential. Playwright warns that a state file may contain sensitive cookies and headers that can impersonate the account. Add playwright/.auth/ to .gitignore, restrict filesystem permissions, encrypt any backup, and regenerate the file after expiration or suspected exposure. Do not print its JSON in CI logs, upload it as an unrestricted artifact, or commit it to a repository.
When a persistent profile is the right tool
Use launchPersistentContext when the browser profile itself must survive closing and reopening—for example, a desktop workflow that depends on profile preferences or browser-managed data. Give each test identity its own directory and never point automated tests at your everyday profile.
import { chromium } from '@playwright/test';
const context = await chromium.launchPersistentContext('./pw-profile-test-user', {
headless: false
});
const page = await context.newPage();
await page.goto('https://app.example.test/account');
// Run the workflow, then close the context when finished.
await context.close();
A persistent profile is not a substitute for isolation. Do not run two processes against the same profile directory, and do not reuse a profile across unrelated accounts. For CI and most parallel suites, a setup project plus storageState gives clearer boundaries and easier cleanup.
Handle sessionStorage deliberately
sessionStorage is scoped to an origin and is not persisted across page loads by Playwright. There is no direct Playwright persistence API for it. If your application genuinely stores authentication data there, serialize it after login and install an initialization script before the application’s first page load.
import { test as setup } from '@playwright/test';
import fs from 'node:fs/promises';
const authFile = 'playwright/.auth/user.json';
const sessionFile = 'playwright/.auth/session-storage.json';
setup('capture session storage', async ({ page, context }) => {
await page.goto('https://app.example.test/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/account');
const values = await page.evaluate(() => {
const result = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
result[key] = sessionStorage.getItem(key);
}
return result;
});
await fs.writeFile(sessionFile, JSON.stringify(values), { mode: 0o600 });
await context.storageState({ path: authFile, indexedDB: true });
});
import { test } from '@playwright/test';
import fs from 'node:fs';
const sessionValues = JSON.parse(
fs.readFileSync('playwright/.auth/session-storage.json', 'utf8')
);
test.beforeEach(async ({ context }) => {
await context.addInitScript(values => {
for (const [key, value] of Object.entries(values)) {
window.sessionStorage.setItem(key, value);
}
}, sessionValues);
});
Inject only the keys the application requires. Copying every entry can restore stale wizard steps, feature flags, or one-time workflow data and make failures appear random.
Automate MFA without weakening it
WebAuthn and passkeys
For a WebAuthn factor, create a dedicated test account and enroll a virtual credential through the same registration flow a real user follows. Keep that credential inside the isolated test environment, then capture it in the controlled browser state if your Playwright version and browser support WebAuthn credential snapshots. Browser-context credential snapshots contain private keys; restoring one installs a virtual authenticator in that context, and real authenticators do not work there.
Microsoft Edge DevTools provides a software virtual authenticator for registration and debugging without a physical key. Use it only for test identities. Your test should still exercise challenge creation, origin checks, user verification requirements, credential registration, sign-in, and credential removal. Do not export a production passkey or turn off MFA merely to make automation easier.
TOTP and other one-time codes
Store the test account’s OTP seed in a secret manager available only to the test worker. Generate a code at run time and submit it through the normal MFA form; never hard-code the seed in a fixture or commit it with the test. Test the security properties, not just the happy path:
- Short expiration time and rejection after expiry.
- Single use: a code accepted once must fail on replay.
- Strict attempt limits, rate limiting, and account or IP lockout.
- Invalidation after successful verification.
- Consistent enforcement on web, API, federated-login, and account-recovery paths.
- No OTP values in application, test, proxy, or CI logs, and no long-term plaintext storage.
FIDO2/WebAuthn is preferable when phishing resistance is part of the threat model because the credential is bound to the legitimate origin. OTP remains useful for testing fallback and recovery paths, but it does not provide the same origin binding.
Reliability and parallel-run practices
- Refresh deliberately: regenerate state when sessions expire instead of retrying a stale file indefinitely.
- Use deterministic identities: one account per worker when tests change server-side state.
- Wait on application signals: prefer a visible post-login element or URL over a fixed sleep.
- Capture required stores only: unnecessary IndexedDB or sessionStorage data increases size and leakage risk.
- Keep MFA enrollment separate: enroll virtual credentials once in a controlled setup, then test sign-in and revocation independently.
- Clean up: remove temporary profiles and expired state files after a run.
A saved state avoids repeated login latency, but it does not make a session permanent. Server-side expiry, revocation, password changes, device policies, and MFA resets can invalidate it at any time.
Troubleshooting common failures
The test is redirected to login
Inspect the post-login context. The application may keep its token in IndexedDB, sessionStorage, or origin-private data rather than cookies or localStorage. Capture the missing store, confirm the state file is loaded by the intended project, and verify that the test origin exactly matches the origin used during login.
State works locally but not in CI
Check filesystem permissions, working-directory differences, environment variables, clock skew, and whether the server binds sessions to IP, device, or user agent. Generate state inside the CI job instead of copying a long-lived file between environments.
Parallel tests log one another out
They are sharing an account or profile. Allocate a state file and server-side identity per worker, or serialize the conflicting tests. Never share one persistent-profile directory between processes.
Rank #4
SessionStorage values disappear
They were injected too late or under the wrong origin. Register addInitScript before navigation and restrict the script to the exact origin that reads the values.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →WebAuthn restoration fails
Confirm that the credential was created for the same test relying party and origin, that the browser context supports the virtual credential snapshot, and that no physical authenticator is expected in a context restored with virtual credentials.
OTP tests pass even when security is broken
Add explicit cases for expiry, replay, excessive attempts, lockout, rate limits, and recovery endpoints. Verify that logs and traces redact the code and seed.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If the goal is to obtain a clean image or PDF of a page rather than drive an interactive login, ScreenshotNeo accepts custom cookies, headers, user agents, and Authorization values in one API request. It removes cookie-consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for authentication and options. A basic request is:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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 includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Create a free ScreenshotNeo account when you want the capture handled without maintaining a browser profile.
Best Value
- Comes with secure packaging
- It can be a gift item
- Easy to read text
FAQ
Can I reuse one state file for every test account?
No. State is identity-specific. Keep a separate snapshot for each account, and separate worker snapshots when tests can interfere with one another.
Does a persistent profile replace MFA enrollment?
No. It preserves profile data; it does not make an expired, revoked, or unenrolled factor valid. Enrollment and recovery still need dedicated test flows.
Should I choose OTP or WebAuthn for new tests?
Use WebAuthn when you need origin-bound, phishing-resistant authentication. Add OTP tests when fallback, recovery, or legacy compatibility is part of the product requirements.
Free tools Windows power users keep installed
One-click scans. No signup required.
Frequently Asked Questions
Can I reuse one state file for every test account?
No. State is identity-specific. Keep a separate snapshot for each account, and separate worker snapshots when tests can interfere with one another.
Does a persistent profile replace MFA enrollment?
No. It preserves profile data; it does not make an expired, revoked, or unenrolled factor valid. Enrollment and recovery still need dedicated test flows.
Should I choose OTP or WebAuthn for new tests?
Use WebAuthn when you need origin-bound, phishing-resistant authentication. Add OTP tests when fallback, recovery, or legacy compatibility is part of the product requirements.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

