VOOZH about

URL: https://dev.to/zerodrop/how-to-e2e-test-sendgrid-email-workflows-in-playwright-11lg

⇱ How to E2E Test SendGrid Email Workflows in Playwright - DEV Community


SendGrid powers transactional email for millions of applications. But how do you test that your SendGrid emails actually arrive, contain the right content, 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 SendGrid:

// app/api/auth/signup/route.ts
import sgMail from '@sendgrid/mail';

sgMail.setApiKey(process.env.SENDGRID_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 sgMail.send({
 from: 'noreply@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 });
}

Stage 1 — Local development: SendGrid sandbox mode

SendGrid has a sandbox mode that intercepts emails without delivering them. Enable it by passing mail_settings in your API call:

await sgMail.send({
 from: 'noreply@yourapp.com',
 to: email,
 subject: 'Verify your email',
 html: `<p>Click <a href="${verifyUrl}">here</a> to verify.</p>`,
 mailSettings: {
 sandboxMode: {
 enable: process.env.NODE_ENV === 'development',
 },
 },
});

In sandbox mode, SendGrid processes the request and validates it but doesn't deliver the email. You can verify delivery in the SendGrid Activity Feed dashboard.

What it solves: Does my app call SendGrid correctly? Is my email template valid?

What it doesn't solve: Automated testing. You can't read emails from SendGrid's Activity Feed programmatically in a test.


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

Switch sandbox mode off for staging:

mailSettings: {
 sandboxMode: {
 enable: false,
 },
},

Emails now go through real delivery. You can manually verify they arrive, links work, and nothing lands in spam. Catches real delivery issues like domain authentication 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: SendGrid live key + ZeroDrop

For automated Playwright tests in GitHub Actions:

npm install zerodrop-client
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 — SendGrid 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');
});

OTP flows

If your app sends a numeric OTP via SendGrid:

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

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

Using SendGrid Dynamic Templates

If you use SendGrid's Dynamic Templates, the flow is the same — ZeroDrop catches the rendered email:

await sgMail.send({
 from: 'noreply@yourapp.com',
 to: email,
 templateId: 'd-your-template-id',
 dynamicTemplateData: {
 verify_url: verifyUrl,
 username: 'Test User',
 },
});
// ZeroDrop catches the fully rendered template output
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
expect(email.magicLink).not.toBeNull();

This tests that your Dynamic Template renders correctly and the link is valid — something sandbox mode can't verify.


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 }}
 SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
 NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}
// Use CI inbox or generate locally
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

The full picture

Sandbox mode (local) Live key (staging) Live key + ZeroDrop (CI)
Validates API call
No real emails sent
Tests template rendering
Automated in CI
Parallel test runs
OTP auto-extraction
Tests real delivery

Use sandbox mode 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