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.