ScreenshotNeo

BlogHow-to

Fix Puppeteer Screenshots That Show the Login Page Instead of the Dashboard

Diagnose whether Puppeteer lost authentication or captured too early, then verify the dashboard is ready before taking a screenshot.

By the ScreenshotNeo team4 October 20268 min read

If Puppeteer captures the login page, either the browser lacks a valid session, the site redirected the dashboard navigation to login, or the dashboard had not rendered when the screenshot was taken. First inspect the final URL and page state, then wait for a dashboard-specific signal and capture only after it appears. A successful page.goto() call by itself does not prove that the application authenticated you.

1. Diagnose the page Puppeteer actually reached

Record the requested URL, final URL, navigation response status, and whether a dashboard-only selector or login-only selector is present. Avoid logging passwords, session cookie values, or authorization headers.

const response = await page.goto('https://example.com/dashboard', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});

console.log({
  requestedUrl: 'https://example.com/dashboard',
  finalUrl: page.url(),
  status: response?.status(),
  redirected: response?.request().redirectChain().length > 0,
});

const dashboard = await page.$('[data-testid="dashboard-root"]');
const login = await page.$('form[data-testid="login-form"]');
console.log({ hasDashboard: Boolean(dashboard), hasLogin: Boolean(login) });

Replace the example selectors with stable selectors from your application. goto() resolves with the response for the last redirect in a redirect chain, so inspect page.url() and the rendered page too. See Puppeteer’s Page.goto API.

2. Ensure login state is available to the page

A new browser context or process may have a fresh profile. Login and dashboard capture must use the same context, or the second run must deliberately restore valid session state or repeat the site’s supported login flow.

Option A: Log in and capture in the same context

This is a runnable pattern for a site with a conventional form; adapt selectors and completion signals to the application. Use a test account and load secrets from environment variables.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const context = await browser.createBrowserContext();
  const page = await context.newPage();
  await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
  await page.locator('input[name="email"]').fill(process.env.TEST_EMAIL);
  await page.locator('input[name="password"]').fill(process.env.TEST_PASSWORD);
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.locator('button[type="submit"]').click(),
  ]);
  await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
  await page.waitForSelector('[data-testid="dashboard-root"]', { timeout: 15_000 });
  await page.screenshot({ path: 'dashboard.png', fullPage: true });
} finally {
  await browser.close();
}

Some single-page applications do not cause a full navigation on login. In that case, wait for a post-login URL or application element instead of waiting for navigation. Puppeteer’s locator API and wait methods are documented in its page interactions guide.

Option B: Reuse a persistent profile

For repeated runs in a controlled environment, launch with a dedicated writable user data directory. Log in through the supported flow once, then reuse that profile according to the site’s policy. Persistence does not keep an expired or revoked session valid.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  userDataDir: './.puppeteer-test-profile',
});
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
  console.log('Final URL:', page.url());
  await page.waitForSelector('[data-testid="dashboard-root"]', { timeout: 15_000 });
  await page.screenshot({ path: 'dashboard.png', fullPage: true });
} finally {
  await browser.close();
}

Keep the profile private because it may contain session data. Puppeteer normally uses a temporary profile; userDataDir selects the profile directory, which must be writable. See Puppeteer troubleshooting and LaunchOptions.

Option C: Restore cookies into the active context

Cookie restoration is appropriate when a test intentionally uses a valid test session and can handle that secret safely. Cookie domain, path, expiry, and context must match; cookies alone may not represent all application state. Use the cookie API for your installed Puppeteer version and verify its current method naming because the cookies documentation may describe a next-version API.

// Illustrative setup: populate values from a secure test secret store.
const context = await browser.createBrowserContext();
await context.setCookie({
  name: 'session',
  value: process.env.TEST_SESSION_COOKIE,
  domain: 'example.com',
  path: '/',
  secure: true,
  httpOnly: true,
  sameSite: 'Lax',
});
const page = await context.newPage();
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });

Do not commit session values or print them in CI logs. Puppeteer’s cookie APIs are described in its cookies guide.

Do not confuse HTTP authentication with a web application session

page.authenticate() supplies credentials for HTTP authentication. It is not a general login mechanism for an application’s form, SSO flow, or session cookie. It also enables request interception behind the scenes. See Page.authenticate.

3. Wait for the dashboard, not just navigation

Network-idle conditions can be unreliable for apps with polling or long-lived requests, and a load event does not guarantee that client-side data has rendered. Wait for a stable element unique to the dashboard, then take the screenshot.

await page.goto('https://example.com/dashboard', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});

try {
  await page.waitForSelector('[data-testid="dashboard-root"]', {
    visible: true,
    timeout: 15_000,
  });
} catch (error) {
  const loginVisible = await page.$('form[data-testid="login-form"]');
  throw new Error(
    `Dashboard did not become ready. URL=${page.url()}, loginVisible=${Boolean(loginVisible)}`
  );
}

await page.screenshot({ path: 'dashboard.png', fullPage: true });

Choose a selector that appears only after the app has completed the relevant dashboard render. Puppeteer’s screenshot guide demonstrates waiting for a selector before capture, and its waitForSelector API documents the wait behavior.

4. Compare the authentication strategies

Approach Use it when Trade-offs
Supported login flow per run Sessions expire quickly, SSO or additional checks apply, or the test covers login More steps and synchronization, but exercises the actual sign-in path
Persistent userDataDir Repeated runs use a controlled environment where retaining a profile is allowed Convenient, but profile access, CI isolation, and expiry need attention
Restore session cookies A valid test session is intentionally provisioned Explicit secret handling; cookie scope and other application state are site-specific

No approach guarantees a valid session in every application. Consider expiry, SSO or MFA behavior, secret handling, CI repeatability, and whether the test should cover authentication itself.

5. Check request interception and redirects

If the script enables request interception, review every handler, especially requests for login endpoints, scripts, and APIs needed to render the dashboard. Each intercepted request must be continued, answered, or aborted exactly once. Puppeteer’s documentation states: “Once request interception is enabled, every request will stall unless it’s continued, responded or aborted.” See the request interception guide.

await page.setRequestInterception(true);
page.on('request', request => {
  if (shouldBlock(request)) {
    return request.abort();
  }
  return request.continue();
});

Make sure shouldBlock does not match authentication, application API, or dashboard asset requests. Avoid installing overlapping handlers that resolve the same request twice.

6. Troubleshooting common failures

Symptom Likely cause What to do
Final URL is the login route Missing, expired, rejected, or out-of-scope session state Use the authorized login flow; verify the same context/profile is used and inspect cookie scope without exposing values.
Final URL is dashboard but screenshot shows login UI Client-side app state did not initialize, or a login overlay remains Wait for a dashboard-only selector and check whether login UI is still present.
goto() resolves, but dashboard is absent The navigation response only confirms the final resource response, not application authentication or readiness Inspect status, final URL, redirects, and rendered selectors.
Selector wait times out Wrong selector, failed authentication, dashboard error, or app still loading On timeout report final URL and presence of login/dashboard selectors; troubleshoot auth before capturing.
Login succeeds locally but fails in CI CI starts with a different profile, missing secret, or a session that is not portable Run the supported test login in CI or provision a valid test session securely; use an isolated writable profile.
Navigation or assets hang after interception is enabled A request was left unresolved or blocked accidentally Audit handlers and ensure every intercepted request is resolved once.
page.authenticate() has no effect on the app login It handles HTTP authentication, not normal application sign-in Use the site’s login flow or valid application session state.

7. Performance, reliability, and cost considerations

  • Prefer a specific readiness selector over arbitrary sleeps. A fixed delay wastes time on fast runs and can still be too short on slow runs.
  • Use explicit navigation and selector timeouts so failures produce a useful diagnosis rather than an indefinite wait.
  • Run login and capture in a clean, isolated context when repeatability matters. A persistent profile can reduce repeated setup but introduces state drift and secret-storage concerns.
  • Capture only after the dashboard assertion passes. Saving a diagnostic screenshot on failure can help, but ensure the page contains no sensitive user data.
  • There is no universal timing or cost figure: application load, authentication steps, browser hosting, and capture size vary. Puppeteer documentation does not establish a failure rate or benchmark for this symptom.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It is useful when you need a page image without maintaining your own browser setup. A single request captures a URL; see the 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

Python:

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)

Node.js:

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}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

For a protected dashboard, the target site must still allow access to the requested page; an API does not make an unauthorized session valid. ScreenshotNeo removes cookie banners, popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

9. FAQ

Why does it work in a normal browser but not in Puppeteer?

The normal browser may have a retained profile and valid session while Puppeteer starts with a fresh context. Compare the profiles and verify the final URL and dashboard state.

Should I save cookies to a file?

Only when the test design requires it and the file can be protected as a secret. Restored state can expire and may be tied to other browser or application state.

Is a longer timeout the fix?

Only if the dashboard is still loading. If navigation ends at the login route, address authentication first; waiting longer will not create a valid session.

Can Puppeteer screenshots include an authenticated dashboard?

Yes, when the page’s browser context has an accepted session and capture waits for the dashboard to render. Follow the application’s access rules and use test credentials for automation.