ZERODROP
← INTEGRATIONSDOCS
PlaywrightClerkGitHub Actions

Testing Clerk Email Flows with Playwright

Clerk sends OTP codes and magic links for email verification. Testing these flows end-to-end in CI requires catching real emails. Here's the complete setup with ZeroDrop.

The problem

Clerk's email verification sends a 6-digit OTP to the user's email address. In CI, you need to catch that email and extract the code automatically. ZeroDrop extracts OTP codes at the edge — your test just reads email.otp, no regex needed.

Install

npm install zerodrop-client

Testing Clerk OTP verification

Clerk's OTP UI renders individual number inputs. The trick: fill only the first input (input[name="code-0"]) and Clerk auto-advances through the rest.

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

const mail = new ZeroDrop();

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

  // 1. Navigate to sign-up
  await page.goto('/sign-up');

  // 2. Fill in email — Clerk uses "identifier" as the field name
  await page.fill('input[name="identifier"]', inbox);
  await page.click('button:has-text("Continue")');

  // 3. Clerk sends OTP to inbox
  await expect(page.locator('input[name="code-0"]')).toBeVisible({ timeout: 5000 });

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

  expect(email.otp).not.toBeNull();

  // 5. The Clerk OTP trick — fill first input, it auto-advances
  await page.fill('input[name="code-0"]', email.otp!);

  // 6. Should be signed in
  await expect(page).toHaveURL('/dashboard');
});

Testing Clerk magic link sign-in

test('Clerk magic link sign-in', async ({ page, context }) => {
  const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

  await page.goto('/sign-in');
  await page.fill('[name="emailAddress"]', inbox);
  await page.click('[data-localization-key="formButtonPrimary"]');

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

  // Open magic link in same context to preserve session
  const newPage = await context.newPage();
  await newPage.goto(email.magicLink!);

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

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 }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          NEXT_PUBLIC_URL: ${{ secrets.STAGING_URL }}

Ready to test your Clerk flows?

Free tier. No signup. No Docker. Works in CI in 5 minutes.

OPEN INBOX →

RELATED GUIDES

Playwright + NextAuthPlaywright + Auth0Full Playwright guide