VOOZH about

URL: https://dev.to/zerodrop/how-to-e2e-test-postmark-email-workflows-in-playwright-b51

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


Postmark is known for fast, reliable transactional email delivery. But how do you test that your Postmark 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 Postmark:

// app/api/auth/signup/route.ts
import { ServerClient } from 'postmark';

const client = new ServerClient(process.env.POSTMARK_API_TOKEN!);

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 client.sendEmail({
 From: 'noreply@yourapp.com',
 To: email,
 Subject: 'Verify your email',
 HtmlBody: `<p>Click <a href="${verifyUrl}">here</a> to verify your email.</p>`,
 MessageStream: 'outbound',
 });

 return Response.json({ success: true });
}

Stage 1 — Local development: Postmark test message stream

Postmark has a dedicated test message stream that accepts emails without delivering them. Change MessageStream from outbound to outbound with a test server token:

// Use Postmark's test API token for local development
const client = new ServerClient(
 process.env.NODE_ENV === 'development'
 ? 'POSTMARK_API_TEST' // Postmark's built-in test token
 : process.env.POSTMARK_API_TOKEN!
);

Postmark's POSTMARK_API_TEST token accepts all emails and returns a success response without delivering anything. You can inspect sent emails in your Postmark dashboard under the test server.

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

What it doesn't solve: Automated testing. You can't read emails from Postmark's test server programmatically in a Playwright test.


Stage 2 — Staging: Postmark live token to a real inbox

Switch to your live Postmark server token for staging:

const client = new ServerClient(process.env.POSTMARK_API_TOKEN!);

Emails now go through Postmark's real delivery infrastructure. You can manually verify they arrive, links work, and the content is correct. Catches real issues like missing DKIM records or template rendering bugs.

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 a real inbox your test can read.


Stage 3 — CI: Postmark live token + 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 — Postmark 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 Postmark:

await client.sendEmail({
 From: 'noreply@yourapp.com',
 To: email,
 Subject: 'Your verification code',
 HtmlBody: `<p>Your code is: <strong>${otp}</strong></p>`,
 MessageStream: 'outbound',
});
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 Postmark Templates

If you use Postmark's template system:

await client.sendEmailWithTemplate({
 From: 'noreply@yourapp.com',
 To: email,
 TemplateAlias: 'verify-email',
 TemplateModel: {
 verify_url: verifyUrl,
 product_name: 'YourApp',
 },
 MessageStream: 'outbound',
});
// ZeroDrop catches the fully rendered template output
const email = await mail.waitForLatest(inbox, { timeout: 30000 });
expect(email.magicLink).not.toBeNull();

This tests that your Postmark template renders correctly with real data — something the test token 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 }}
 POSTMARK_API_TOKEN: ${{ secrets.POSTMARK_API_TOKEN }}
 NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}
// Use CI inbox or generate locally
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

The full picture

Test token (local) Live token (staging) Live token + 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 the test token during development, the live token for manual staging verification, and the live token + ZeroDrop for automated CI.


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