Practices3 min read

Visual testing authenticated pages

The pages that matter most sit behind a login. How to hold a session across a visual run, seed the data that makes captures repeatable, handle SSO and MFA, and keep screenshots of real accounts safe.

VisualMax EngineeringProduct team

Most visual testing guidance quietly tests marketing pages, because they are easy. The pages where a broken layout actually costs you money — the dashboard, settings, billing, checkout — are behind a login and full of live data. That is why they get skipped, and why skipping them means the suite is protecting the surface with the least business risk.

Authenticate once, reuse the session

Logging in through the UI before every test is slow, and it makes the login form a single point of failure for your entire visual suite. Authenticate once and reuse the stored state.

ts
// auth.setup.ts — runs once, before the visual project.
import { test as setup, expect } from "@playwright/test";

const AUTH_FILE = "playwright/.auth/user.json";

setup("authenticate", 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: "Log in" }).click();

  // Wait for the post-login state, not just the navigation.
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();

  await page.context().storageState({ path: AUTH_FILE });
});
ts
// playwright.config.ts
projects: [
  { name: "setup", testMatch: /auth\.setup\.ts/ },
  {
    name: "visual",
    use: { storageState: "playwright/.auth/user.json" },
    dependencies: ["setup"],
  },
],

The data problem is bigger than the auth problem

Getting logged in is the easy half. The hard half is that an authenticated page renders someone's data, and that data changes — which means the screenshot changes, and no threshold fixes it.

  1. Seed a fixed dataset for the test account. Highest fidelity, because real rendering paths run against real shapes of data. This is the right answer where you can afford it.
  2. Stub the API responses so the page renders from fixtures. Faster and more isolated, but you stop testing the real data path.
  3. Mask the dynamic regions. A last resort — every mask is coverage you have given up.

Seeding is worth the investment because it also gives you the states nobody thinks to test:

  • Empty state — a new account with no projects. Frequently the most broken screen in a product.
  • Single item — layouts designed for grids often look wrong with one entry.
  • Overflow — 200 rows, a 60-character name, a very long email address.
  • Error state — a failed payment banner, an expired token warning.
  • Permission variants — what an admin sees versus a read-only member.

Each of these is a distinct visual state of the same route, and each has broken in production for someone. A suite that only captures the happy path with tidy fixture data is testing the screen least likely to be wrong.

SSO, MFA, and the awkward cases

Real authentication is rarely a form post.

  • MFA — issue the test account a fixed TOTP secret and generate codes in the setup step, or exempt that one account from the second factor in non-production environments.
  • SSO — point the test environment at a test identity provider rather than automating a third-party login page you do not control and that will change without warning.
  • Magic links — read the token from your mail-catcher API (Mailpit, Mailhog) rather than a real inbox.
  • Short session expiry — re-run the setup project rather than raising expiry globally in a way that could reach production.

Treat the screenshots as sensitive

A capture of an authenticated page contains whatever was on screen. If your test account holds anything resembling real customer data, your artifact store now holds it too — in an unencrypted PNG, in CI, for whatever your retention period is.

  • Seed synthetic data. Never point a visual test account at production or a production clone.
  • Give the test account the narrowest permissions that still reach the pages you cover.
  • Keep artifact retention short — seven days is usually plenty for a review workflow.
  • Do not run authenticated visual jobs on fork pull requests. Forks do not get your secrets, correctly; do not work around that with pull_request_target.
  • Rotate the test credentials on the same schedule as any other service credential.

The CI-side specifics — gating jobs on the pull request source, retention settings, and the fork pattern — are in visual regression testing in GitHub Actions.

Frequently asked questions

How do you visually test pages behind a login?

Authenticate once in a setup project, save the browser storage state to a file, and have the visual project reuse it via the storageState option. That avoids logging in through the UI for every test and stops the login form being a single point of failure for the whole suite.

How do you handle changing data in authenticated visual tests?

Seed a fixed dataset for the test account where possible, since that preserves real rendering paths and lets you cover empty, single-item, overflow, and error states. Stubbing API responses is faster but tests less. Masking dynamic regions is a last resort because every mask is coverage you give up.

Is it safe to screenshot authenticated pages in CI?

Only with care. Screenshots capture whatever was on screen, so use synthetic seeded data rather than production, give the test account minimal permissions, keep artifact retention short, rotate credentials regularly, and never run authenticated visual jobs on fork pull requests.

Referenced by