ScreenshotNeo

BlogHow-to

How to Run Visual Regression Tests on Pages Behind a Login

Authenticate once, save browser state safely, and compare stable screenshots of protected pages with reviewed baselines.

By the ScreenshotNeo team4 October 20269 min read

To run visual regression tests on a page behind a login, authenticate a dedicated test account during setup, save its browser state securely, load that state in your test, navigate to the protected route, wait for a page-specific ready condition, and compare a screenshot with an approved baseline. A screenshot by itself is only a capture: regression testing also requires comparison and review of visual changes.

This guide uses Playwright Test for a complete runnable example, then explains the Cypress route, stability controls, account security, troubleshooting, and when a capture API can help.

1. Set up a dedicated test identity

Create a test account with the minimum role and permissions needed for the page under test. Avoid a personal or production account. If the test changes server-side data, isolate accounts by worker or test run so concurrent runs cannot affect each other. For role-based pages, create separate authenticated states for each role.

Playwright’s recommended pattern is to authenticate in a setup project, save the browser context state, and reuse it in dependent tests. The saved state can contain cookies and headers that identify the account, so keep it out of source control and treat it as a secret. See the official Playwright authentication guide.

2. Configure Playwright authentication and visual comparison

Install Playwright Test and its browser binaries in your project using the official installation instructions. Add the authentication directory to .gitignore:

# .gitignore
playwright/.auth/

Use this minimal configuration. Replace the example base URL and route with your application’s values. The setup project runs first; the protected-page project depends on it.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        baseURL: 'https://app.example.test',
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

Create the state directory locally or in your CI job before the setup test runs, and ensure it is writable by the test process:

mkdir -p playwright/.auth

The setup test signs in through the UI, waits for a reliable logged-in signal, then saves state. Use your real labels and success condition. Prefer a visible account menu or authenticated landing page over a fixed delay.

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

const authFile = 'playwright/.auth/user.json';

setup('authenticate test account', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL ?? '');
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD ?? '');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Replace this with a stable signal that proves this account is signed in.
  await expect(page.getByRole('navigation', { name: 'Account' })).toBeVisible();

  fs.mkdirSync('playwright/.auth', { recursive: true });
  await page.context().storageState({ path: authFile });
});

Supply TEST_USER_EMAIL and TEST_USER_PASSWORD through your local environment or your CI secret store. Do not hard-code real credentials or commit the generated state file.

3. Capture a protected page and compare it to a baseline

With the saved state configured, Playwright creates a context that already has the test account’s browser state. Navigate directly to the protected route, assert a page-specific ready condition, then use Playwright Test’s screenshot assertion. On the first run, the assertion writes a reference image; later runs compare against it. Review the generated baseline and commit an approved one with the test code.

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

test('account page matches its visual baseline', async ({ page }) => {
  await page.goto('/account');

  // Wait for a meaningful state, not an arbitrary sleep.
  await expect(page.getByRole('heading', { name: 'Account overview' })).toBeVisible();
  await expect(page.getByTestId('account-summary')).toBeVisible();

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

Run the setup and test projects with:

npx playwright test

To intentionally create or update reference screenshots after reviewing a known UI change, use Playwright’s snapshot update mode:

npx playwright test --update-snapshots

Do not accept every changed image automatically. Keep the old baseline when a difference is a defect; update it only when the visual change is intended and reviewed. Read the official Playwright visual comparisons guide for assertion options and snapshot behavior.

4. Make screenshots stable and useful

Visual comparisons are sensitive to rendering conditions and page content. A useful baseline represents a known application state captured in a consistent environment.

  • Wait for readiness: Assert a page-specific heading, row, or state that indicates the content is rendered. If a required API response is part of readiness, wait for that response or assert the resulting UI. Avoid relying on a fixed sleep as the main readiness check.
  • Control data: Seed test records or stub volatile responses with fixtures where appropriate. Use the same account and data shape for baseline generation and comparison.
  • Control animation and time: Disable animation in screenshot assertions where supported. Freeze time or stabilize relative timestamps when the page displays clocks, countdowns, or “updated moments ago” labels.
  • Keep the environment consistent: Use the same browser, operating system, fonts, viewport, device scale factor, and headless settings for baseline creation and CI comparisons. Rendering can vary across machines and browser configurations.
  • Choose focused checkpoints: Snapshot meaningful page states or components. A full-page capture is useful for layout coverage; a component screenshot is often easier to review when a particular area matters.
  • Mask sparingly: Mask only small regions that are truly unpredictable, such as a third-party widget. Broad masks and permissive thresholds can conceal real regressions.

Playwright screenshot assertions accept options such as fullPage, animations, and locator-level assertions; consult the official snapshot documentation for the current option set. Pick a viewport and capture scope that reflect the behavior you need to protect instead of taking every possible screenshot.

5. Use Cypress with a visual comparison integration

Cypress can drive the application into an authenticated state and capture it, but cy.screenshot() alone does not compare images against a baseline. Add a visual-testing integration when you need comparison and an approval workflow. Cypress documents integrations including Applitools, Percy, Sauce Labs Visual, and SmartBear VisualTest; choose based on framework support, capture scope, review workflow, browser coverage, data policy, and current pricing. Prices and plans change, so verify them with the provider before choosing.

A Cypress test can establish the logged-in state before the capture using your application’s normal test authentication flow. The exact comparison command depends on the integration you select:

// Cypress example: capture after the application is authenticated and ready.
describe('protected account page', () => {
  beforeEach(() => {
    cy.loginAsTestUser(); // Define this custom command for your approved test login flow.
    cy.visit('/account');
    cy.findByRole('heading', { name: 'Account overview' }).should('be.visible');
  });

  it('captures the account page for visual review', () => {
    // This captures an image; add your selected visual integration's
    // comparison/checkpoint command to compare it with an approved baseline.
    cy.screenshot('account-overview');
  });
});

cy.loginAsTestUser() is deliberately a project-specific custom command, not a built-in Cypress command. Define it to use the application’s approved test login path, and ensure it waits for a reliable authenticated condition. Follow your chosen integration’s official documentation for its checkpoint, baseline, and approval APIs. Cypress’s visual testing guide describes the capture, comparison, and review lifecycle.

6. Protect authentication state and isolate test data

  • Keep Playwright state files such as playwright/.auth/user.json out of Git. Restrict access to them in CI artifacts and caches.
  • Use test-only accounts and non-production data. Avoid destructive actions against production identities or records.
  • Regenerate stored state when credentials, session policy, or account permissions change. If a session expires, rerun authentication setup rather than weakening the application’s access checks.
  • When parallel tests mutate shared data, assign separate accounts or otherwise isolate records by worker or run. Reusing a single account is appropriate only when concurrent tests cannot conflict.
  • For MFA, SSO, CAPTCHA, or device-bound sessions, use the approved test environment and identity-provider setup. There is no universal safe automation bypass; coordinate the test path with the team that manages the application and identity provider.

7. Troubleshooting

Symptom Likely cause Fix
Redirected to the login page The state file was not loaded, the setup project did not run, the session expired, or the app uses storage not covered by the saved state. Confirm the configured storageState path, run the setup dependency, and inspect whether the application’s login session is stored in browser cookies or local storage. Regenerate state through the normal login flow.
Authentication setup times out The selectors do not match, a login challenge is waiting, or the test waits on a brittle condition. Use labels and roles that match the real form, inspect the trace or page, and wait for a stable post-login signal. Resolve MFA or SSO requirements through the approved test setup.
State file cannot be written The target directory does not exist or the CI process lacks write permission. Create playwright/.auth before saving and check the job’s working directory and file permissions.
Snapshots differ on every run Dynamic content, animation, fonts, viewport, browser, or environment differs. Stabilize fixture data and time, disable animation, wait for readiness, and generate and compare baselines in the same browser and environment.
False differences appear in a widget A third-party widget, ad, or live region changes independently of the application. Stub or disable it in the test environment if appropriate, or mask only its small unpredictable region. Keep the rest of the page visible to comparison.
Parallel runs interfere with one another Tests share an account or mutable server-side records. Use separate test identities or isolate data by worker and run. Saved browser state does not isolate server-side account changes.
cy.screenshot() creates files but reports no regression Cypress’s capture command does not itself compare images with an approved baseline. Configure a visual testing integration and add its comparison/checkpoint command and review workflow.

8. Performance, reliability, and cost

Most of the time in this workflow is spent starting the browser, authenticating, rendering the page, and waiting for stable content. Reusing saved state avoids repeating the interactive login in each test, while a setup project still refreshes state as part of the run. Keep the number of checkpoints focused on important states; every additional page or viewport adds capture and comparison work.

Reliability comes from deterministic data, explicit readiness assertions, isolated accounts, and consistent baseline environments. A baseline is an expectation that future runs must review against, so bulk-accepting changes makes the check less useful. Hosted visual services can add collaboration, browser coverage, or review workflows, but their capabilities and costs vary; compare current provider documentation and pricing before adopting one. The reviewed sources do not establish current prices for those services.

Or skip the browser setup

If your goal is to capture a public page for documentation or monitoring, ScreenshotNeo can return a screenshot from one GET request. It does not replace authenticated visual regression testing: this endpoint call does not log into your application, compare against a baseline, or approve differences. For protected pages, keep authentication in your controlled test browser unless you deliberately use an approved public capture route.

See the ScreenshotNeo API documentation. Example cURL, Python, and Node.js calls:

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn more at ScreenshotNeo or sign up free for 1,000 screenshots a month, no card required.

FAQ

Can I save a logged-in session once and reuse it?

Yes, when the session remains valid and the account’s server-side state is safe to share. Store the browser state as a secret, and regenerate it when the session expires or the authentication policy changes.

Does a screenshot assertion tell me whether a change is good?

No. It identifies a visual difference from the reference. A person or an explicitly approved review process must decide whether that difference is intended.

Should every test use a full-page screenshot?

No. Use full-page captures for page-level layout and focused component captures for localized behavior. Choose checkpoints that protect meaningful states without creating noisy, hard-to-review diffs.

Can an external screenshot API test a private page behind login?

A simple URL-to-image request does not authenticate into your private application. Use a controlled browser test with approved authentication state for protected routes; use a screenshot API for pages it can reach without that private session.