VOOZH about

URL: https://dev.to/zerodrop/otp-verification-in-playwright-without-regex-d3i

⇱ OTP Verification in Playwright Without Regex - DEV Community


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.

zerodrop.dev