React Email14 min read

React Email Preview Testing: Local Dev, CI, and Cross-Client Automation

Build a complete testing workflow for React Email templates: local preview servers, automated unit tests, visual regression with Playwright, cross-client testing (Gmail, Outlook, Apple Mail), and CI integration.

R

React Emails Pro

March 1, 2026

React Email has a preview server. Most developers start it once, check that the email looks okay, and then ship.

That's fine for early prototypes. But in production, you need to know that your templates work across clients, with real data, and when props change. Manual spot-checking doesn't scale.

This guide covers three testing workflows: local preview testing, automated CI checks, and cross-client visual testing. Pick the ones that match your risk tolerance.

Local preview testing: the dev server workflow

React Email includes a preview server that hot-reloads your templates. It's the fastest feedback loop for design and layout work.

terminal
# Start the preview server
npm run email dev

# Or if you configured it differently
npx react-email dev

The server runs on http://localhost:3000 by default and shows all your email templates with sample props.

Preview data: build factories, not inline objects

Most developers inline preview props at the top of each template. That works until you need to test edge cases or reuse preview data in tests.

Better pattern: centralize preview data in a factory file.

emails/preview-data.ts
export const previewData = {
  passwordReset: {
    base: {
      userName: "Alex",
      resetLink: "https://app.example.com/reset/abc123",
      expiresInMinutes: 15,
    },
    longName: {
      userName: "Alexandria Konstantinopolous-Weatherby",
      resetLink: "https://app.example.com/reset/abc123",
      expiresInMinutes: 15,
    },
    expiringSoon: {
      userName: "Alex",
      resetLink: "https://app.example.com/reset/abc123",
      expiresInMinutes: 1,
    },
  },
  welcome: {
    base: {
      userName: "Jordan",
      dashboardLink: "https://app.example.com/dashboard",
      supportEmail: "help@example.com",
    },
    noName: {
      userName: undefined,
      dashboardLink: "https://app.example.com/dashboard",
      supportEmail: "help@example.com",
    },
  },
};

Now your template imports clean preview data, and your tests can reuse the same fixtures.

emails/password-reset.tsx
import { previewData } from "./preview-data";

export default function PasswordResetEmail(props: PasswordResetProps) {
  // Template code...
}

// Preview props for the dev server
PasswordResetEmail.PreviewProps = previewData.passwordReset.base;
Create variants for edge cases: missing names, long text, expired links, RTL languages. If you haven't tested it in preview, it will break in production.

Automated testing: catch breaks before deployment

Preview servers are manual. Automated tests catch regressions during code review.

Unit tests: validate props and rendering

Use Vitest (or Jest) to test that your email templates handle props correctly and don't crash on missing data.

emails/__tests__/password-reset.test.tsx
import { describe, it, expect } from "vitest";
import { render } from "@react-email/render";
import PasswordResetEmail from "../password-reset";
import { previewData } from "../preview-data";

describe("PasswordResetEmail", () => {
  it("renders with valid props", () => {
    const html = render(
      <PasswordResetEmail {...previewData.passwordReset.base} />
    );
    expect(html).toContain("Reset your password");
    expect(html).toContain(previewData.passwordReset.base.resetLink);
  });

  it("handles missing userName gracefully", () => {
    const html = render(
      <PasswordResetEmail
        {...previewData.passwordReset.base}
        userName={undefined}
      />
    );
    expect(html).toContain("Reset your password");
    expect(html).not.toContain("undefined");
  });

  it("includes expiration warning when < 5 minutes", () => {
    const html = render(
      <PasswordResetEmail {...previewData.passwordReset.expiringSoon} />
    );
    expect(html).toContain("expires soon");
  });

  it("does not crash with malformed props", () => {
    expect(() => {
      render(
        <PasswordResetEmail
          userName="Alex"
          resetLink=""
          expiresInMinutes={-1}
        />
      );
    }).not.toThrow();
  });
});

These tests are fast, run in CI, and catch the most common template failures: undefined values, broken conditionals, missing fallbacks.

Schema validation: enforce props at the boundary

Pair unit tests with schema validation (Zod, Yup) to catch bad data before render.

emails/password-reset.tsx
import { z } from "zod";

const PasswordResetPropsSchema = z.object({
  userName: z.string().optional(),
  resetLink: z.string().url(),
  expiresInMinutes: z.number().positive(),
});

export type PasswordResetProps = z.infer<typeof PasswordResetPropsSchema>;

export default function PasswordResetEmail(props: PasswordResetProps) {
  // Validate at runtime (throws if invalid)
  const validated = PasswordResetPropsSchema.parse(props);
  
  return (
    // Use validated props...
  );
}
Schema validation at render time prevents silent failures. If you skip it, broken URLs and malformed data will reach production inboxes.

Visual regression testing: catch layout breaks

Unit tests validate logic. Visual regression tests validate appearance.

Use Playwright to screenshot your emails in the preview server, then compare against baseline images.

tests/email-screenshots.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Email templates visual regression", () => {
  test.beforeEach(async ({ page }) => {
    // Assumes preview server is running on localhost:3000
    await page.goto("http://localhost:3000");
  });

  test("password reset email matches baseline", async ({ page }) => {
    await page.click('text="PasswordResetEmail"');
    await page.waitForSelector('iframe[title="Preview"]');
    
    const frame = page.frameLocator('iframe[title="Preview"]');
    await expect(frame.locator("body")).toHaveScreenshot("password-reset.png");
  });

  test("welcome email matches baseline", async ({ page }) => {
    await page.click('text="WelcomeEmail"');
    await page.waitForSelector('iframe[title="Preview"]');
    
    const frame = page.frameLocator('iframe[title="Preview"]');
    await expect(frame.locator("body")).toHaveScreenshot("welcome.png");
  });
});

On first run, Playwright captures baseline screenshots. On future runs, it compares new renders against baselines and fails if pixels differ.

Run visual tests in CI with --update-snapshots disabled. Only update baselines deliberately after reviewing changes locally.

Cross-client testing: Gmail, Outlook, Apple Mail

The hardest part of email development: what looks perfect in preview can break in Outlook, Gmail mobile, or dark mode.

You need to test real clients. Here are three practical approaches.

Option 1: Litmus or Email on Acid (paid)

Services like Litmus and Email on Acid render your emails in 90+ real email clients and provide side-by-side screenshots.

  • Pros: comprehensive, includes dark mode and mobile clients
  • Cons: expensive ($99+/month), manual workflow

Option 2: Self-hosted email testing (MailHog + real accounts)

Use MailHog or Mailpit to capture emails locally, then forward them to real Gmail, Outlook, and Apple Mail accounts for manual review.

terminal
# Run MailHog locally
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog

# Configure your app to send to localhost:1025
# Visit http://localhost:8025 to view captured emails

Forward each email to:

  • A Gmail account (test web + mobile app)
  • An Outlook.com account (test web + Windows client)
  • An iCloud account (test Apple Mail on macOS/iOS)
Create dedicated test accounts for email development. Don't pollute your personal inbox with dozens of test sends.

Option 3: Automated cross-client testing with Playwright

If you need repeatable cross-client testing in CI, you can automate Gmail and Outlook web clients with Playwright.

tests/cross-client.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Cross-client email rendering", () => {
  test("password reset renders correctly in Gmail", async ({ page }) => {
    // Send test email to Gmail test account
    await sendTestEmail({
      to: "test@gmail.com",
      template: "password-reset",
    });

    // Log into Gmail and check rendering
    await page.goto("https://mail.google.com");
    await page.fill('input[type="email"]', process.env.GMAIL_TEST_EMAIL);
    await page.click('button:has-text("Next")');
    // ... full login flow ...

    // Open the email and screenshot
    await page.click('text="Reset your password"');
    await expect(page).toHaveScreenshot("gmail-password-reset.png");
  });
});

This is advanced and brittle (Gmail UI changes break tests), but it's the only way to automate real client testing without a paid service.


CI integration: run tests on every PR

Add email tests to your CI pipeline so broken templates never reach main.

.github/workflows/email-tests.yml
name: Email Tests

on:
  pull_request:
    paths:
      - 'emails/**'
      - 'tests/emails/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Run email unit tests
        run: npm run test:emails

      - name: Start preview server
        run: npm run email dev &
        env:
          CI: true

      - name: Wait for preview server
        run: npx wait-on http://localhost:3000

      - name: Run visual regression tests
        run: npx playwright test tests/email-screenshots.spec.ts

      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: test-results/
Keep email tests fast. Run visual regression only on email file changes, not on every commit.

Dark mode testing: don't forget it

Gmail, Outlook, and Apple Mail all apply dark mode transformations. Sometimes they invert your colors. Sometimes they don't.

Add dark mode variants to your preview data and test manually.

emails/password-reset.tsx
// Use color-scheme meta tag to hint at dark mode support
<head>
  <meta name="color-scheme" content="light dark" />
</head>

// Use inline styles that work in both modes
<div style={{
  backgroundColor: "#ffffff",
  color: "#000000",
  // Avoid pure black/white that clients invert badly
}}>
  {/* Content */}
</div>

Test dark mode manually in:

  • Gmail (web + mobile app with dark theme enabled)
  • Outlook (Windows client with dark mode)
  • Apple Mail (macOS with system dark mode)
Some clients invert images in dark mode. Use transparent PNGs or SVGs with explicit colors to avoid logo inversions.

Pre-deployment checklist

Before you ship a new email template, run through this checklist:

  • ✅ Preview all variants in dev server (base + edge cases)
  • ✅ Unit tests pass (props validation + rendering)
  • ✅ Visual regression tests pass (layout unchanged)
  • ✅ Sent test email to Gmail, Outlook, Apple Mail
  • ✅ Checked dark mode rendering in all three clients
  • ✅ Verified links work (no localhost URLs)
  • ✅ Plain text version generated correctly
  • ✅ Subject line and preheader set

If you skip steps, your first production send will be your cross-client test. Don't do that.


When to test (and when to ship fast)

Not every email needs the full testing gauntlet. Here's how to prioritize:

  • High-stakes emails (password reset, verification, billing): full test suite, cross-client checks, visual regression
  • Marketing emails (newsletters, announcements): preview + one real client test
  • Internal notifications (admin alerts, logs): preview only, ship fast
The goal isn't perfect test coverage. The goal is not breaking password resets because you changed a CSS variable.

Start with unit tests + preview. Add visual regression when template changes cause production incidents. Add cross-client testing when users complain about rendering.

For production-ready templates that come pre-tested, check out our SaaS template library — all core flows are already validated across Gmail, Outlook, and Apple Mail.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates