ScreenshotNeo

BlogHow-to

How to Test Pages Behind a Login with Screenshot Regression Tests

Reuse a safe authenticated browser state, capture stable screenshots, and review visual diffs without hiding real regressions.

By the ScreenshotNeo team4 October 20268 min read

To test a page behind a login, authenticate a browser context before the visual assertion, then capture a named screenshot only after the intended signed-in state and page content are ready. Keep the authentication state secret, use stable test data and a fixed rendering environment, and review visual diffs before approving a new baseline. Authentication gets the test to the right page; screenshot comparison detects changes after it gets there.

For a project already using Playwright Test, its built-in toHaveScreenshot() is a direct way to compare against reference images. Cypress can capture screenshots with cy.screenshot(); add a visual testing plugin or integration for baseline comparison and review.

1. Set up a safe authenticated test state

Use a dedicated test environment and account. Decide whether tests can safely share that account: if tests modify server-side data that other tests read, isolate them with separate accounts or otherwise independent data. Concurrent tests against shared mutable state can interfere with each other.

Playwright recommends saving browser state during a setup step and loading it into tests. The state can contain cookies and headers that impersonate the account, so keep it out of source control and treat it as a secret. The example below uses a local playwright/.auth directory.

# .gitignore
playwright/.auth/

Install Playwright Test if the project does not already include it, then create a setup test that signs in and saves storage state. Replace the example selectors and route with those used by your application.

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'node:path';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.E2E_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.E2E_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Assert login completed before saving the state.
  await expect(page.getByRole('heading', { name: 'Account overview' })).toBeVisible();
  await page.context().storageState({ path: authFile });
});

Supply credentials through your CI secret store or local environment, not source code. For example, in a shell environment configure E2E_USER_EMAIL and E2E_USER_PASSWORD before running the tests. Do not print them in logs.

Configure a setup project and make the visual tests depend on it. Load the saved state and use a consistent viewport. Use the same browser, operating system, browser version, and headless configuration to generate and compare reference images whenever possible; rendering can vary across environments.

// 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'],
        baseURL: 'http://127.0.0.1:3000',
        storageState: 'playwright/.auth/user.json',
        viewport: { width: 1440, height: 900 },
      },
      dependencies: ['setup'],
    },
  ],
});

The app server must be available at the configured base URL, and the setup project must create the auth file before dependent tests run. Keep this file in a private workspace. If you intentionally need to check login itself, make that a separate test that does not load the already-authenticated state.

2. Assert the page is ready, then compare it

Navigate directly to the protected route when the test is about its logged-in rendering. First make a functional assertion that confirms the expected page state. Then take a named screenshot. Playwright’s screenshot assertion waits for consecutive screenshots to match before saving, which helps avoid capturing a page while it is still changing.

// tests/account-visual.spec.ts
import { test, expect } from '@playwright/test';

test('account overview matches the approved design', async ({ page }) => {
  await page.goto('/account/overview');

  await expect(page.getByRole('heading', { name: 'Account overview' })).toBeVisible();
  await expect(page.getByTestId('account-balance')).toContainText('$');

  await expect(page).toHaveScreenshot('account-overview.png', {
    fullPage: true,
  });
});

On the first run, Playwright creates the reference image; later runs compare new captures with it. Review the generated reference and commit only approved baselines. When the UI intentionally changes, inspect the diff and update the baseline through the project’s chosen workflow; never update references automatically just to make a failing run pass.

Run the suite in the same environment used to maintain the references. For example, use the project’s normal Playwright command to run the setup and dependent project in CI. Check the Playwright documentation for the current commands and configuration details: authentication and visual comparisons.

3. Make screenshots stable and useful

  • Wait for meaningful state. Prefer a visible heading, loaded record, or completed status assertion over an arbitrary sleep. Cypress puts it plainly: “Take a snapshot only after you confirm the page is done changing.” (Cypress visual testing.)
  • Control data that changes. Use repeatable fixtures or stub API responses where appropriate. Keep dates, account records, and other dynamic inputs predictable.
  • Control rendering inputs. Fix the viewport and keep browser, operating system, fonts, and headless mode consistent between baseline and comparison.
  • Avoid animation captures. Wait until transitions finish or disable animations in the test environment where that is appropriate.
  • Mask narrowly. If an uncontrolled third-party region cannot be stabilized, mask or hide that small region. Do not loosen a whole-page comparison threshold to accommodate one volatile widget.
  • Choose the right scope. Use full-page screenshots to test page layout relationships; capture a focused element when the component is the subject. Keep checkpoints meaningful so review remains actionable.
  • Review every difference. A changed baseline can represent an intentional redesign or a regression. Inspect the diff before accepting it.
  • Keep accessibility checks separate. Pixel comparison does not determine whether text contrast meets accessibility standards. Pair visual tests with appropriate accessibility checks.

4. Cypress alternative

Cypress can drive the app into a logged-in state and capture a screenshot. Its built-in screenshot command captures; it does not itself compare the image to a baseline. Add a visual testing integration if you need approved-reference comparison and a diff review workflow.

// Cypress example: adapt login commands and selectors to your app.
describe('account overview visual state', () => {
  beforeEach(() => {
    cy.visit('/login');
    cy.get('[name=email]').type(Cypress.env('E2E_USER_EMAIL'));
    cy.get('[name=password]').type(Cypress.env('E2E_USER_PASSWORD'), { log: false });
    cy.get('button[type=submit]').click();
    cy.contains('Account overview').should('be.visible');
  });

  it('captures the signed-in page', () => {
    cy.visit('/account/overview');
    cy.contains('Account overview').should('be.visible');
    cy.screenshot('account-overview');
    // Configure a visual comparison integration to compare this capture
    // with an approved reference and report/review differences.
  });
});

Store credentials in Cypress environment or CI secrets and avoid logging them. Cypress automatically captures screenshots on test failures during cypress run according to its screenshot configuration, but failure screenshots are diagnostic artifacts, not by themselves visual regression assertions. See the Cypress screenshots and videos guide and its visual testing guide.

5. Troubleshooting

Symptom Likely cause Fix
Protected route redirects to login The saved state was not created, has expired, or does not include the authentication mechanism used by the app. Check that setup completed and asserted the signed-in page before saving. Recreate the state, verify the correct auth file path, and confirm the app’s login/session behavior.
Auth file is missing in CI The setup project did not run, its output path differs, or a workspace cleanup removed the file. Make the test project depend on setup, use a consistent relative path, and ensure the setup and dependent tests share the same workspace.
Tests pass locally but fail visual comparison in CI Browser, OS, fonts, viewport, headless mode, or rendering settings differ. Generate and compare references in the same CI image and browser configuration. Pin the viewport and browser version through the project’s normal dependency setup.
Intermittent image diffs Capture occurs before app data settles, or dates, animation, fonts, or third-party content vary. Wait for a functional ready condition, control responses and test data, disable or await animation, and narrowly mask unavoidable volatile regions.
Different records appear across parallel tests Tests share an account or mutable backend state. Use independent accounts or isolated records for tests that mutate data; avoid concurrent writes to shared fixtures.
Baseline update hides an unexpected change Reference images were refreshed without review. Restore the prior approved baseline, inspect the diff, and update only after confirming the intended UI change.
Screenshot looks correct but accessibility issue remains Visual diffs assess pixels, not semantic accessibility requirements. Add accessibility checks for contrast and other relevant requirements alongside visual regression tests.

6. Or skip the browser setup

If the goal is a screenshot of a publicly reachable page, ScreenshotNeo can capture it through one GET request. It is a screenshot API and MCP server from Yorker Media. A screenshot API does not replace the authenticated browser state in the test above: for a private route, you still need a supported way to provide the authorized session. ScreenshotNeo supports custom cookies, headers, and Authorization, so use only credentials you are permitted to send and follow 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
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 fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for ScreenshotNeo free.

7. Performance, reliability, and cost

Visual test runtime grows with browser startup, authentication setup, page loading, and screenshot comparison. Reuse saved authentication state so each test need not repeat the interactive login flow, while refreshing it when sessions expire. Keep the number of screenshots focused on meaningful routes and states. Full-page captures cover more layout but can take longer and produce larger artifacts than a focused element capture.

For reliability, make setup failure explicit, assert the expected signed-in state, and ensure CI runs setup before dependent tests. Treat auth state and screenshots as artifacts with appropriate access controls; screenshots may expose account data. Keep test data isolated when tests write to shared systems.

Playwright’s local screenshot assertions do not introduce a per-capture ScreenshotNeo charge; the operational costs are CI time and storing/reviewing artifacts. If using a hosted visual testing integration, check that provider’s current pricing and retention terms directly. ScreenshotNeo pricing is $0 for 1,000 shots/month, 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; annual billing gives two months free, and every feature is on every plan.

FAQ

Should the screenshot test also test that login works?

Usually keep those concerns distinct. Test the login flow separately, then bootstrap visual tests with saved authenticated state so a login-flow failure does not obscure page-rendering comparisons.

Can screenshot assertions prove the page is correct?

They can detect rendered pixel changes against an approved reference. They do not prove business logic, correctness of all content, or accessibility compliance, so retain functional and accessibility assertions.

When should I use full-page capture?

Use it when vertical layout and relationships across the page matter. For a single component or panel, a focused element capture creates a narrower, easier-to-interpret checkpoint.