DEV Community

Jeffrey Marvin Forones
Jeffrey Marvin Forones

Posted on

Testing OTP email flows shouldn't be flaky — meet AssertKit

The single most flake-prone test in any E2E suite is "user signs up,
verifies via emailed OTP, completes onboarding." Why? Because the email
is async, the test runner can't see it, and the bridge between the two
is a 60-line polling helper everyone writes once and nobody wants to
maintain.

I shipped AssertKit to remove that bridge entirely.

The pitch in 8 lines of test code:

  import { test } from "@assertkit/playwright";

  test("signup with OTP", async ({ page, inbox }) => {
    await page.goto("/signup");
    await page.getByLabel("Email").fill(inbox.address);
    await page.getByRole("button", { name: "Sign up" }).click();
    const otp = await inbox.waitForOtp({ from: "noreply" });
    await page.getByLabel("Code").fill(otp);
  });
Enter fullscreen mode Exit fullscreen mode

That's it. No polling. No regex. No flake.

How it works

  1. The inbox fixture gives every test a unique disposable address (foo-1717xxx@assertkit.com).
  2. waitForOtp long-polls the AssertKit server for up to 25 seconds.
  3. The server watches for inbound mail to that address, extracts any OTP-shaped code, and returns it as a typed field.
  4. Your test gets a string, not an email body to parse.

Cypress users: same package, same shape:

  import "@assertkit/cypress";

  cy.uniqueInbox().then((inbox) => {
    cy.visit("/signup");
    cy.get('input[name="email"]').type(inbox.address);
    cy.get('button[type="submit"]').click();
    cy.waitForOtp(inbox.local).then((otp) => {
      cy.get('input[name="otp"]').type(otp);
    });
  });
Enter fullscreen mode Exit fullscreen mode

Try the live demo (no signup):
https://assertkit.com/demo

The demo opens a real long-poll against the real wait endpoint. A
canned email arrives 3 seconds later. You'll see the OTP land in real
time.

Free to try:

  • Both npm packages are MIT, zero runtime deps
  • The free public API works without an account
  • Subscriptions aren't open yet — there's a /beta program for early access with admin approval

Tech stack (if anyone's curious): Next.js 16 App Router, Drizzle +
Neon Postgres, Supabase Realtime for sub-100ms broadcast, CloudMailin
for inbound MX, Resend for outbound (DKIM-signed), Upstash KV for
rate limits, argon2id + RFC 6238 TOTP for auth.

Solo founder, happy to answer anything.

Top comments (0)