Playwright Screenshot Shows the Sign-In Form After Login: Diagnose and Fix Redirects
If a Playwright screenshot shows the sign-in form after login, verify the redirect and signed-in state, then check which browser storage your app needs.
If a Playwright screenshot shows a sign-in form after a test appears to log in, first check whether the login flow reached its final success condition. A click on “Sign in” does not prove authentication finished: the app may still be setting cookies across redirects, the saved browser state may be incomplete or expired, or the app may redirect based on its own rules.
Wait for a known authenticated URL or a stable signed-in UI element before saving state or taking the screenshot. Then confirm the test context has the storage your app actually uses. The exact cause depends on the app, Playwright version, state file, and redirect sequence; the symptom alone cannot identify it.
1. Confirm login reached the authenticated page
Replace the example URL and selector below with stable conditions from your application. Prefer an exact destination URL and a meaningful signed-in control, such as an account menu or dashboard heading. Avoid relying on the click completing or adding an arbitrary sleep: neither confirms that authentication succeeded.
import { test, expect } from '@playwright/test';
test('sign in and capture the dashboard', async ({ page }) => {
await page.goto('https://example.com/sign-in');
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();
// Use the final URL your application reaches after authentication.
await expect(page).toHaveURL('https://example.com/dashboard');
// Assert a stable, app-specific sign that the user is signed in.
await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible();
await page.screenshot({ path: 'dashboard.png', fullPage: true });
});
If the destination contains a variable query string or path segment, use a regular expression or a predicate that checks the relevant URL parts. Keep the UI assertion too: an application can reach a dashboard URL while still rendering an error or logged-out state.
2. Save authentication state after success
For a suite that can safely share one account, authenticate in a setup project, save storage state only after the success checks pass, and reuse it in tests. This example uses Playwright Test and TypeScript.
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const statePath = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('https://example.com/sign-in');
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).toHaveURL('https://example.com/dashboard');
await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible();
await page.context().storageState({ path: statePath });
});
// 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'],
},
],
});
Add the generated state directory to .gitignore:
playwright/.auth/
Storage state can contain cookies and other sensitive values that may let someone act as the account. Keep it out of source control and handle it like a credential. If the state expires, rerun the setup to generate a fresh file. See the official Playwright authentication guide.
3. Check which browser storage the app uses
Playwright storage state covers cookies and local storage. Its API can also include IndexedDB when requested; the supported options depend on the installed Playwright version. The standard storage-state workflow does not automatically preserve session storage. If login depends on session storage, a state file with cookies and local storage can look valid while a new context still appears logged out.
| App authentication data | What to check | Next step |
|---|---|---|
| Cookies | Are cookies present after the final redirect? Do domain, path, Secure, and SameSite settings allow them on the protected route? | Save state after the final success condition and inspect the cookie scope and expiry. |
| Local storage | Does the app store its token under the expected origin? | Save state from the correct origin after login completes. |
| IndexedDB | Does the app keep authentication data there? | Use the documented IndexedDB state option if your installed Playwright version supports it. |
| Session storage | Does the app rely on values scoped to a tab or browsing session? | Use an app-appropriate initialization script to restore it; standard storage state does not include it. |
| Server-side session or identity provider | Does the session expire, require a fresh challenge, or depend on a particular redirect/callback? | Complete the real flow and validate the final state before saving. Consider API-based authentication if the app supports it. |
For session storage, use an initialization script that runs before the application reads the values. The exact keys and values are application-specific; do not copy credentials into a committed fixture.
// Example: restore app-specific session storage before navigating to the app.
const savedSession = { 'app-session-key': process.env.APP_SESSION_VALUE! };
await page.addInitScript((entries) => {
for (const [key, value] of Object.entries(entries)) {
sessionStorage.setItem(key, value as string);
}
}, savedSession);
await page.goto('https://example.com/dashboard');
Review Playwright’s BrowserContext storage-state API for the options supported by your installed version. The authentication guide also covers API authentication, multiple roles, and per-worker state.
4. Choose shared or per-worker authentication
- Shared state: Use one setup-generated state file when tests can use the same account without changing conflicting server-side data.
- Per-worker state: Use separate accounts and state per worker when parallel tests mutate account data or otherwise interfere with one another.
- API authentication: If the application has a supported test login endpoint, create authenticated state through the API, then verify it in a browser. This can avoid repeating a UI login flow, but still requires the right browser storage and app-specific success checks.
- Multiple roles: Create a separate state for each role and load the appropriate one in each test. A user state cannot prove that an administrator or another role is authenticated.
These approaches follow the choices in the official authentication guide. Pick based on whether accounts can be shared safely, whether tests run in parallel, and how the application establishes identity.
5. Capture useful evidence when it still fails
Record the final URL, the authenticated UI assertion, and a screenshot at the point the test fails. A full-page screenshot includes content below the viewport; a locator screenshot isolates the sign-in form.
import { test, expect } from '@playwright/test';
test('capture login redirect evidence', async ({ page }) => {
// Run the login steps here.
try {
await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible();
} catch (error) {
console.error('Observed URL:', page.url());
await page.screenshot({ path: 'failure-page.png', fullPage: true });
const signInForm = page.getByRole('form', { name: 'Sign in' });
if (await signInForm.count()) {
await signInForm.screenshot({ path: 'sign-in-form.png' });
}
throw error;
}
});
Playwright documents screenshots as a way to inspect rendered layout and record bugs. For visual regression checks, toHaveScreenshot waits for consecutive screenshots to stabilize before comparing with a baseline and requires the Playwright Test runner. See the screenshots documentation and PageAssertions reference.
6. Troubleshoot by symptom
| Symptom | Likely cause to investigate | Fix or diagnostic |
|---|---|---|
| The click succeeds, then the test saves state immediately | Redirects or cookie writes have not completed. | Wait for the final URL and a stable signed-in element before saving state. |
| The setup passes, but tests show sign-in | Wrong state path, wrong browser context, or state created for a different origin. | Confirm the exact file path in config and the origin that created the state; inspect state presence without exposing secrets in logs. |
| It worked yesterday, then began redirecting | Session expired, was revoked, or changed due to server-side policy. | Regenerate state through the normal login flow and confirm the setup success checks still pass. |
| Cookies and local storage exist, but the app is logged out | The app may depend on IndexedDB, session storage, or another app-specific mechanism. | Identify the actual auth storage in the app and persist or restore it using supported, version-appropriate APIs. |
| Only parallel runs fail | Workers may share and mutate the same account or server-side session. | Use separate test accounts or per-worker state when tests change shared account data. |
| The browser reaches the dashboard URL but shows sign-in | The app may render a logged-out state at that URL, or a client-side auth check may redirect after load. | Assert the signed-in UI as well as URL; inspect the URL and page after client rendering settles. |
| Only one browser or environment fails | Browser-specific behavior, environment config, or identity-provider policy may differ. | Compare traces, final URLs, state origin, and the app’s auth checks across the failing and working environments. |
| Session-storage restoration has no effect | The script runs after app code reads storage, or the wrong origin/key is used. | Register the init script before navigation and match the application origin and expected keys. |
Do not infer a root cause from the screenshot alone. Compare the observed URL, when state was saved, the storage mechanism, the Playwright version, and the app’s redirect sequence.
7. Keep screenshots and auth state reliable
- Wait on application state, not a fixed delay. Explicit URL and UI assertions make failures easier to diagnose and avoid unnecessary waiting.
- Save state only after the final authentication check. A successful form submission is an intermediate event.
- Refresh expired state and ensure the setup runs before dependent projects.
- Use isolated accounts when parallel tests modify shared server-side state.
- Keep state files and credentials out of version control, artifacts, and logs that other users can access.
- Use full-page capture only when below-the-fold content matters; otherwise a viewport screenshot is smaller and quicker to inspect.
- For visual assertions, keep the browser, viewport, and rendering environment consistent with the baseline because rendering differences can change pixels.
8. Or skip the browser setup
If the goal is a clean capture of a public page rather than a screenshot of an authenticated test session, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await Bun.write('shot.webp', bytes);
Cookie banners, popups, and chat widgets are removed before the shot, and 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 provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month, no card required.
FAQ
Does a screenshot prove that login succeeded?
No. A screenshot shows what rendered. Pair it with assertions for the final URL and a stable authenticated control.
Can I reuse one authentication file across browsers?
Use state only where it matches the application and browser context. If browser-specific behavior matters, validate authentication in each target browser and generate state as needed.
Will ScreenshotNeo capture a page that requires my Playwright login?
The one-call example captures a URL; it does not reproduce a private Playwright test session. Use Playwright when you need to test an authenticated flow, or review the API documentation for supported request options.


