DEV Community

Cover image for E2E email testing is a nightmare. I’m trying to fix the middle ground.
Hari
Hari

Posted on

E2E email testing is a nightmare. I’m trying to fix the middle ground.

If you write Playwright or Cypress tests, you already know the exact moment the joy leaves your body.

Testing the UI is easy. But the second your test runner hits a "Check your email for the login code" screen, everything falls apart.

I know the email testing space is already heavily saturated, but if you're just trying to verify an auth flow, developers are still forced into two terrible options:

  1. The duct-tape method: Wiring up a shared Gmail account via IMAP, writing brittle polling scripts, and praying that concurrent tests don't steal each other's OTPs.
  2. The overkill method: Adopting massive, expensive enterprise email QA platforms that feel like bringing a bazooka to kill a fly.

There is a gap for a lightweight tool that just does one thing: provisions an isolated inbox on the fly, hands you the OTP or magic link, and gets out of your way.

So, I built PostMX to fill that gap.

I am launching the V1 today, and to be completely transparent, I have exactly zero users. But I want to share the architecture of how it works, because even if you never touch my API, moving to ephemeral inboxes is the only way to stop your CI pipeline from being flaky.

Instead of a static inbox, you treat the email address as a temporary API object. You spin up a fresh inbox for a specific test run, trigger your app, and grab the extracted data as clean JSON. No Regex, no IMAP rate limits.

Here is what the execution looks like using the Node SDK I just shipped:

import { test, expect } from '@playwright/test';
import { PostMX } from "postmx";

const postmx = new PostMX(process.env.POSTMX_API_KEY);

test('User can sign in via magic link', async ({ page }) => {
  // 1. Spin up a fresh, isolated inbox just for this test run
  const inbox = await postmx.createTemporaryInbox({ label: "signup-test" });

  await page.goto('https://your-app.com/login');

  // 2. Use the ephemeral email address in your UI
  await page.fill('input[name="email"]', inbox.email_address);
  await page.click('button[type="submit"]');

  // 3. Wait for the email and grab the first extracted link
  const email = await postmx.waitForMessage(inbox.id);
  const magicLink = email.links[0]; 

  // 4. Complete the auth flow
  await page.goto(magicLink);
  await expect(page.locator('.dashboard')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

I wanted this to be the lowest-friction way possible to get tests passing reliably.

The API and the docs are live at postmx.co.

Since I'm starting from absolute zero today, I would genuinely appreciate any harsh feedback. Please roast the implementation, the docs, or tell me why this architecture wouldn't work for your specific CI pipeline. I'll be in the comments answering everything.

Top comments (4)

Collapse
 
bridgexapi profile image
BridgeXAPI

This hits 😅 I ran into similar issues testing OTP flows where everything works locally, but breaks once real delivery is involved. Mocking feels too clean, but full E2E gets messy fast with timing and retries. Spinning up a fresh inbox per test actually makes a lot of sense. Curious how stable it stays under parallel tests?

Collapse
 
hharanm profile image
Hari

Yeah, mocking auth is basically just crossing your fingers for prod 😅

Parallel stability was actually the main goal here. It handles it perfectly because every test worker spins up a unique inbox ID on the fly. There's zero shared state, so you completely eliminate race conditions or cross-test contamination.

Let me know if you end up throwing it into your pipeline!

Collapse
 
bridgexapi profile image
BridgeXAPI

yeah makes sense, no shared state fixes a lot already. seen similar issues with OTP flows where parallel runs start stepping on each other 😅 this sounds like a clean way to handle it

Collapse
 
hharanm profile image
Hari

Readers, let me know if you have any questions!