Every developer who has written a Playwright test for OTP verification has written this line:
const otp = email.body.match(/\b\d{6}\b/)?.[0];
It works. Until it doesn't.
The email body changes format. The OTP appears inside an HTML table. The sending service wraps it in a <span>. Your regex matches a phone number instead of the code. The test fails intermittently and you spend an hour debugging something that has nothing to do with the feature you're testing.
The regex problem
OTP extraction via regex is brittle by nature. You're pattern-matching against a string that your email sending service controls — not you. Any time the template changes, your tests break.
Here's what a typical OTP test looks like today:
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
test('user can verify OTP', async ({ page }) => {
const inbox = mail.generateInbox();
// 1. Trigger OTP send
await page.goto('/login');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// 2. Wait for email
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
// 3. Extract OTP — the fragile part
const otp = email.body.match(/\b\d{6}\b/)?.[0];
if (!otp) throw new Error('OTP not found in email body');
// 4. Enter OTP
await page.fill('[data-testid="otp"]', otp);
await page.click('[data-testid="verify"]');
await expect(page).toHaveURL('/dashboard');
});
The test works — but line 14 is carrying all the risk. Change the email template and the test breaks. Add a phone number to the footer and the regex matches the wrong number. Send a 4-digit OTP instead of 6 and you need to update the pattern.
OTP extraction at the edge
ZeroDrop extracts OTPs before they reach your test. The Cloudflare Worker that catches incoming emails runs a pattern match on the plain-text body and stores the result alongside the raw email in Redis.
When your test calls waitForLatest, the extracted OTP is already there as a first-class field:
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
email.otp // "123456" — already extracted
email.magicLink // "https://..." — verification links too
email.body // raw body still available if you need it
Both fields are null if not detected. No regex needed in your test code.
The same test, without regex
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';
const mail = new ZeroDrop();
test('user can verify OTP', async ({ page }) => {
const inbox = mail.generateInbox();
// 1. Trigger OTP send
await page.goto('/login');
await page.fill('[data-testid="email"]', inbox);
await page.click('[data-testid="submit"]');
// 2. Wait for email — OTP already extracted
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
expect(email.otp).not.toBeNull();
// 3. Enter OTP
await page.fill('[data-testid="otp"]', email.otp!);
await page.click('[data-testid="verify"]');
await expect(page).toHaveURL('/dashboard');
});
The fragile regex line is gone. The test asserts that the OTP exists and uses it directly. If the email template changes, the extraction logic at the edge updates independently of your test code.
What gets extracted
The edge worker extracts:
OTP codes — standalone 4-8 digit numeric codes. Detected when they appear near common labels like code, otp, pin, verification, or as isolated numbers on their own line.
Magic links — verification or reset URLs containing path segments like verify, confirm, reset, token, activate, or auth. The first matching URL is extracted.
Both are stored with the email payload in Redis and expire after 30 minutes along with the rest of the inbox.
In GitHub Actions
The same fields are available when using the GitHub Action:
- name: Generate test inbox
id: inbox
uses: zerodrop-dev/create-inbox@v1
- name: Run OTP tests
run: npx playwright test
env:
TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
// In your test
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();
const email = await mail.waitForLatest(inbox, { timeout: 15000 });
// OTP ready to use
await page.fill('[data-testid="otp"]', email.otp!);
Parallel OTP tests
Because every inbox is isolated and OTPs are extracted per-inbox, parallel test runs work without coordination. 10 workers testing OTP flows simultaneously get 10 isolated inboxes with 10 independently extracted codes.
// Safe to run in parallel — each inbox is isolated
const inboxes = Array.from({ length: 10 }, () => mail.generateInbox());
No race conditions, no shared state, no cleanup between runs.
Install
npm install zerodrop-client
No signup. No Docker. No SMTP config. Free tier includes OTP extraction, magic link detection, and SSE-based sub-second email delivery in CI.
Top comments (0)