DEV Community

Silviu Technology
Silviu Technology

Posted on

Playwright Email Retries Without False Positives

Playwright Email Retries Without False Positives

Email verification tests often fail for two opposite reasons: the email arrives late, or the test passes after reading the wrong message. In QA, both failures matter. A flaky red build wastes time, but a false green build is worse because it teaches the team to trust the wrong signal.

I started treating retryable email tests as an evidence problem, not just a timeout problem. If a Playwright test retries, I want every attempt to prove which user triggered the message, which run observed it, and why that message counts as the expected one. That sounds strict, but it saves a lot of "works on my machine" arguing later.

Why retries create misleading passes

Retries are useful when infrastructure is noisy. They are risky when your assertions are too broad. A common pattern is:

  1. Create an account.
  2. Wait for "any" verification email.
  3. Retry the test if the message does not arrive fast enough.

That flow can pass on the second attempt by consuming the first attempt's message. The run looks green, but the application may still be sending duplicate emails, delayed emails, or badly correlated emails.

This is why I prefer tracking a run-scoped identifier from the UI action to the inbox assertion. It is similar to the idea behind these email resend regression checks: test the exact behavior you care about, not a nearby side effect.

The failure pattern I look for first

Before changing waits, I inspect three things:

  1. Whether each retry creates a unique email address or alias.
  2. Whether the application writes a delivery correlation id into logs or metadata.
  3. Whether the inbox polling logic filters by recipient, subject, and creation time.

If one of those is missing, retries get messy real fast. Teams then paste in bigger timeouts, and the suite feels calmer for a week... until another race shows up.

I also watch for typo-driven debug habits. If someone is manually searching a staging inbox for temp gamil com or calling a helper tempail, that is usually a signal the workflow around test mailboxes is already a bit improvised.

A retry-safe Playwright structure

The fix is not huge. The main idea is to make each attempt own its inbox identity and its expected evidence window.

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

test("signup sends one valid verification email", async ({ page }, testInfo) => {
  const attempt = testInfo.retry;
  const runId = `${testInfo.parallelIndex}-${attempt}-${Date.now()}`;
  const inbox = `signup-${runId}@example.test`;

  await page.goto("/signup");
  await page.getByLabel("Email").fill(inbox);
  await page.getByLabel("Password").fill("Str0ngPass!123");
  await page.getByRole("button", { name: "Create account" }).click();

  const message = await waitForVerificationEmail({
    recipient: inbox,
    afterTs: Date.now() - 5_000,
    subjectIncludes: "Verify your account"
  });

  expect(message.recipient).toBe(inbox);
  expect(message.html).toContain("/verify?token=");
});
Enter fullscreen mode Exit fullscreen mode

What matters here is not the exact helper name. What matters is the contract:

  • New inbox identity per attempt.
  • Narrow time window.
  • Subject filter plus recipient filter.
  • Assertions against the expected message, not the first message found.

When teams ask me how to get temporary email coverage into E2E tests without turning the suite fragile, this is where I start. If you use a hosted inbox provider such as tempmailso, keep it as a verification boundary, not as the source of truth for application state.

Where a temporary inbox helps

A temporary inbox is useful for confirming that the outside-world email experience still works: rendering, links, subject lines, and delivery sequencing. It should not replace app-side telemetry. I still want server logs, event ids, and provider responses available when the test fails.

That split also helps during a privacy review for email events. Inbox tools are for validation. Logs are for diagnosis. Mixing those two jobs into one system gets confusing fast, and kinda expensive too.

One extra reason to stay disciplined: retry-heavy suites can hide duplicate sends. According to the Playwright docs, retries rerun the failed test in a fresh worker process, which is great for isolation but easy to misuse if the surrounding email state is shared (https://playwright.dev/docs/test-retries). Fresh worker, stale inbox, false confidence.

A short checklist before you enable retries

  • Generate a unique mailbox per test attempt.
  • Record an app-side correlation id when the email is queued.
  • Filter inbox queries by recipient and a tight time window.
  • Save the raw message id when assertions fail.
  • Review whether the retry masked a duplicate-send defect.
  • Keep manual debug shortcuts out of the long-term test design.

This checklist is a bit boring, honestly, but boring systems are easier to trust. In QA, that is a feature.

Q&A

Should I just increase the timeout?

Only if you already know the assertion is precise. Longer waits can reduce noise, but they do not fix ambiguous inbox matching.

Do retries belong in email tests at all?

Yes, sometimes. External delivery is noisier than local DOM assertions. Just make the retry evidence strict enough that a rerun cannot accidentally bless the wrong message.

What is the first signal that my setup is too loose?

If one inbox is reused across tests, or if the assertion accepts the first matching subject from any recent run, your suite is probly passing for the wrong reasons at least some of the time.

Top comments (0)