ZERODROP
← INTEGRATIONSDOCS
Next.jsGitHub ActionsPlaywright

Testing Next.js Email Flows in GitHub Actions

A complete setup for testing email verification, OTP, and password reset flows in your Next.js app — running in GitHub Actions CI with no Docker, no shared inboxes, and no mocking.

The problem

Next.js apps with email flows — signup verification, magic links, OTP codes, password resets — are notoriously hard to test in CI. The standard approaches either require running a Docker SMTP container (slow, adds cold start time) or mocking the email layer entirely (doesn't test real delivery).

ZeroDrop gives each GitHub Actions run a fresh disposable inbox over HTTPS. No Docker service block. No shared state between parallel runs.

Install

npm install zerodrop-client

The GitHub Actions workflow

name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:

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

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

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Build Next.js app
        run: npm run build
        env:
          NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}

      - 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 }}
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

Email verification test

import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();

test('signup and verify email', async ({ page }) => {
  const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

  await page.goto('/signup');
  await page.fill('[name="email"]', inbox);
  await page.fill('[name="password"]', 'TestPass123!');
  await page.click('[type="submit"]');

  // Catch verification email — magic link auto-extracted
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });
  expect(email.magicLink).not.toBeNull();

  await page.goto(email.magicLink!);
  await expect(page).toHaveURL('/dashboard');
});

OTP verification test

test('OTP login', async ({ page }) => {
  const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

  await page.goto('/login');
  await page.fill('[name="email"]', inbox);
  await page.click('[type="submit"]');

  // Catch OTP email — code auto-extracted, no regex
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });
  expect(email.otp).not.toBeNull();

  await page.fill('[name="otp"]', email.otp!);
  await page.click('[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
});

Password reset test

test('password reset flow', async ({ page }) => {
  const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

  await page.goto('/forgot-password');
  await page.fill('[name="email"]', inbox);
  await page.click('[type="submit"]');

  // Catch reset email
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });
  expect(email.magicLink).not.toBeNull();

  await page.goto(email.magicLink!);
  await page.fill('[name="password"]', 'NewPass123!');
  await page.click('[type="submit"]');

  await expect(page.getByText('Password updated')).toBeVisible();
});

Parallel matrix builds

Each matrix job gets its own isolated inbox automatically:

jobs:
  test:
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - name: Generate test inbox
        id: inbox
        uses: zerodrop-dev/create-inbox@8706a59

      - name: Run tests
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          TEST_INBOX: ${{ steps.inbox.outputs.inbox }}

Ready to test your Next.js email flows in CI?

Free tier. No signup. No Docker. Works in GitHub Actions in 5 minutes.

OPEN INBOX →

RELATED GUIDES

Playwright + NextAuthPlaywright + ClerkPlaywright + Auth0GitHub Actions guideFull Playwright guide