DEV Community

Cover image for How to E2E Test Resend Email Workflows in Playwright
zerodrop
zerodrop

Posted on

How to E2E Test Resend Email Workflows in Playwright

Resend is the standard for transactional email in modern Next.js and React apps. But how do you test that your Resend emails actually arrive, contain the right verification link, and work end-to-end in CI?

This guide covers the full testing progression — from local development to automated Playwright tests in GitHub Actions.


The app we're testing

A Next.js API route that sends a verification email via Resend:

// app/api/auth/signup/route.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const { email } = await req.json();

  const token = crypto.randomUUID();
  const verifyUrl = `${process.env.NEXT_PUBLIC_URL}/verify?token=${token}`;

  await resend.emails.send({
    from: 'hello@yourapp.com',
    to: email,
    subject: 'Verify your email',
    html: `<p>Click <a href="${verifyUrl}">here</a> to verify your email.</p>`,
  });

  return Response.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

Stage 1 — Local development: Resend test API key

Resend's re_test_ API keys capture every email in their dashboard without sending to real inboxes. No email leaves Resend's servers.

RESEND_API_KEY=re_test_xxx npm run dev
Enter fullscreen mode Exit fullscreen mode

Sign up in your browser, check the Resend dashboard — the email appears with full HTML preview. Perfect for template development.

What it solves: Does my app call Resend correctly? Does the template render?

What it doesn't solve: Automated testing. Your Playwright test can't read Resend's dashboard.


Stage 2 — Staging: Resend live key to a real inbox

Switch to a live key for staging environment testing:

RESEND_API_KEY=re_live_xxx
Enter fullscreen mode Exit fullscreen mode

Emails now go through real delivery infrastructure. You can manually verify that emails arrive, links work, and nothing lands in spam. Catches real delivery issues like domain configuration problems.

What it solves: Does the email actually reach a real inbox end-to-end?

What it doesn't solve: Automation. You can't run this in CI without access to a real inbox your test can read.


Stage 3 — CI: Resend live key + ZeroDrop

For automated Playwright tests in GitHub Actions, you need a disposable inbox your test can read programmatically:

npm install zerodrop-client
Enter fullscreen mode Exit fullscreen mode
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();

test('user can sign up and verify email', async ({ page }) => {
  // 1. Generate a disposable inbox
  const inbox = mail.generateInbox();
  // → "swift-x7k2m@zerodrop-sandbox.online"

  // 2. Sign up — Resend sends a real verification email to this inbox
  await page.goto('/signup');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  await expect(page).toHaveURL('/check-email');

  // 3. ZeroDrop catches the email — magic link auto-extracted
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });

  expect(email.subject).toContain('Verify your email');
  expect(email.magicLink).not.toBeNull();

  // 4. Click the verification link
  await page.goto(email.magicLink!);

  // 5. Assert verified
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Email verified')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Resend's live infrastructure delivers the email. ZeroDrop catches it at Cloudflare's edge. The test reads it automatically.


OTP flows

If your app sends a numeric OTP via Resend:

await resend.emails.send({
  from: 'hello@yourapp.com',
  to: email,
  subject: 'Your verification code',
  html: `<p>Your code is: <strong>${otp}</strong></p>`,
});
Enter fullscreen mode Exit fullscreen mode
const email = await mail.waitForLatest(inbox, { timeout: 30000 });

// OTP auto-extracted — no regex needed
expect(email.otp).not.toBeNull();
await page.fill('[data-testid="otp"]', email.otp!);
await page.click('[data-testid="verify"]');
Enter fullscreen mode Exit fullscreen mode

GitHub Actions workflow

name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      - run: npx playwright install --with-deps chromium

      - name: Generate test inbox
        id: inbox
        uses: zerodrop-dev/create-inbox@8706a59 # v1.0.0

      - name: Run E2E tests
        run: npx playwright test
        env:
          TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
          RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
          NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}
Enter fullscreen mode Exit fullscreen mode
// Use CI inbox or generate locally
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();
Enter fullscreen mode Exit fullscreen mode

The full picture

Test key (local) Live key (staging) Live key + ZeroDrop (CI)
Visual inspection ✅ dashboard ✅ real inbox ✅ automated
No real emails sent
Automated in CI
Parallel test runs
OTP auto-extraction
Tests real delivery

Use the test key during development, the live key for manual staging verification, and the live key + ZeroDrop for automated CI.


ZeroDrop — disposable email inboxes for CI pipelines. Free, no signup, no Docker.
zerodrop.dev · docs · npm

Top comments (0)