Framework

Next.js visual regression testing that doesn't rot

Next.js apps have unique challenges for visual testing: SSR hydration, streaming, and data fetching everywhere. Learn how to build stable screenshot tests that catch real regressions.

Why Next.js is tricky for visual tests

Next.js is powerful, but that power introduces complexity for visual testing. Server rendering, client hydration, and streaming mean your page goes through multiple visual states before stabilising.

SSR/CSR mismatch

Server-rendered HTML and client-hydrated UI can differ subtly. Screenshots captured too early show server state; too late shows client state.

Hydration timing

React hydration isn't instantaneous. Interactive elements may render differently before and after hydration completes.

Streaming and Suspense

React Server Components and streaming mean content appears progressively. Screenshots may capture partial states.

Dynamic data everywhere

Next.js makes it easy to fetch data everywhere. Each data source is a potential source of visual variance.

Most visual test failures in Next.js apps aren't real regressions—they're timing issues. Your tests captured different states of the same page.

Making pages deterministic

The key to stable Next.js visual tests is eliminating variance. Control everything that could differ between test runs.

Seed your data

Use consistent test fixtures. Mock database queries to return the same data every run. Avoid random IDs visible in UI.

Mock external APIs

Intercept fetch calls to return deterministic responses. Real APIs change, rate limit, and time out unpredictably.

Freeze time

Mock Date and timestamps. "Last updated 2 minutes ago" changes every time you run tests.

Disable animations

CSS transitions and Framer Motion cause timing variance. Disable them in your test environment.

See reducing visual testing flakiness for detailed techniques.

Handling hydration

React hydration is the process of attaching event handlers to server-rendered HTML. During hydration, the page may visually shift as client-side JavaScript takes over.

To capture stable screenshots after hydration:

// Wait for hydration to complete
await page.goto('/your-page');

// Wait for network idle (fetches complete)
await page.waitForLoadState('networkidle');

// Wait for a specific interactive element
await page.waitForSelector('[data-hydrated="true"]');

// Or wait for React to finish
await page.evaluate(() => {
  return new Promise((resolve) => {
    requestIdleCallback(resolve, { timeout: 5000 });
  });
});

Consider adding a data-hydrated attribute to your root component that flips to true after hydration. This gives your tests a reliable signal.

Mocking data in Next.js

Next.js fetches data at multiple levels: Server Components, route handlers, client-side fetches. You need to mock all of them for consistent screenshots.

For Server Components and route handlers, use MSW or similar:

// In your Playwright test setup
import { test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  // Intercept API calls
  await page.route('**/api/**', async (route) => {
    const url = route.request().url();

    if (url.includes('/api/users')) {
      return route.fulfill({
        json: { users: [{ id: 1, name: 'Test User' }] },
      });
    }

    return route.continue();
  });
});

For time-dependent content, freeze the clock at a specific timestamp in your test fixtures.

Component vs page-level screenshots

You have two choices for where to capture screenshots:

  • Page-level screenshots capture the full rendered page. They catch integration issues but are more prone to flakiness and produce large diffs when any component changes.
  • Component-level screenshots capture individual components in isolation. They're more stable and diffs are focused, but they miss integration issues.

For Next.js, a hybrid approach works well: use Storybook or similar for component visual testing, and Playwright page screenshots for critical user flows only. See Playwright visual testing for flow-level strategies.

PR workflow for visual changes

Next.js apps change frequently. You need an approval workflow that keeps up without becoming a rubber-stamp exercise.

  • Keep PRs focused: Small, single-purpose PRs produce reviewable visual diffs. Monster PRs get approved without review.
  • Link diffs to code: Make it easy to see which code change caused which visual change. PR comments with links help.
  • Automate the happy path: If only baseline files changed (no functional code), consider auto-approving with notification.

See approval workflow patterns for detailed strategies.

Next.js visual testing that doesn't rot

  • Mock all external API calls with deterministic responses
  • Seed database with consistent test fixtures
  • Freeze Date.now() and any timestamp-dependent code
  • Disable CSS transitions and JavaScript animations
  • Wait for hydration to complete before capturing
  • Handle Suspense boundaries—wait for content to load
  • Use consistent fonts (web fonts, wait for loading)
  • Run tests against next start, not next dev
  • Generate baselines from CI, not local machines

Related guides

Frequently Asked Questions

Should I test Server Components or Client Components?
Both, but differently. Server Components are more deterministic—they render once on the server. Client Components need hydration to complete before screenshots are stable. Consider capturing after hydration for Client Components, or testing Server Components in isolation.
How do I handle loading states and Suspense boundaries?
Wait for loading states to resolve before capturing. In Playwright, use waitForLoadState('networkidle') or wait for specific elements to appear. For Suspense, wait for the fallback to be replaced with actual content.
Why do my Next.js screenshots show different content locally vs CI?
Usually data or timing differences. Your local database has different data than CI. API responses differ. Timestamps change. Make your test data entirely deterministic—mock everything that could vary between environments.
Should I use Playwright or Cypress for Next.js visual tests?
Both work. Playwright has better built-in visual comparison and handles modern React features well. Cypress integrates tightly with component testing. For App Router and Server Components, Playwright is often easier to set up.
How do I test Next.js dynamic routes?
Generate a consistent set of test URLs with known data. If your routes are data-driven (like /blog/[slug]), ensure the test data includes stable slugs with predictable content. Test a representative sample, not every possible route.
Can I visual test during next build?
Not directly—next build produces static HTML, but visual testing needs a running browser. Run your visual tests against next start (production mode) or next dev. Production mode is closer to what users see and more deterministic.

Build visual testing workflows for modern Next.js apps

Get early access