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?
How do I handle loading states and Suspense boundaries?
Why do my Next.js screenshots show different content locally vs CI?
Should I use Playwright or Cypress for Next.js visual tests?
How do I test Next.js dynamic routes?
Can I visual test during next build?
Build visual testing workflows for modern Next.js apps
Get early access