惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
有赞技术团队
有赞技术团队
罗磊的独立博客
博客园 - 叶小钗
Jina AI
Jina AI
博客园 - 司徒正美
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
量子位
MyScale Blog
MyScale Blog
V
Visual Studio Blog
博客园 - 聂微东
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
宝玉的分享
宝玉的分享

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
OTP Verification in Playwright Without Regex
zerodrop · 2026-06-15 · via DEV Community
Cover image for OTP Verification in Playwright Without Regex

zerodrop

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