DEV Community

Silviu Technology
Silviu Technology

Posted on

Playwright Password Reset Tests Need Baselines

Password reset tests look simple on paper: click the button, wait for an email, open the reset link, continue. In practice, that step fails in ways that feel random because the mailbox already had history before the test started. A stale message with the same subject can pass one run, then break the next, which is annoyingly common in shared environments.

When I debug these flows, I try to avoid blaming delivery speed too early. Most of the flakes I see come from poor message selection, not from the provider being slow. If your suite uses a fake email address for each run, you are already in better shape. But if the assertion still says "give me the latest reset email," the test is still guessing a bit.

That is why I prefer a baseline step. Before the UI triggers the reset, record the mailbox state. After the click, only accept messages that appear after that snapshot. It sounds basic, maybe even too basic, but it removes a lot of ambiguity and saves time later.

If you have been improving related flows with attempt receipt tracking or thinking about safer oauth inbox testing, this pattern fits right beside them.

Why password reset tests often read the wrong email

Reset flows tend to reuse the same subject line and very similar content. That means a polling helper can easily pick:

  • a message from a previous retry
  • a message from a manual test done five minutes earlier
  • the right user but the wrong reset attempt

I also see teams keep a long-lived dummy e mail inbox for convenience. It is fast to set up, sure, but it creates a messy timeline. Once that inbox has enough history, "latest email" stops being a reliable rule. The test may still pass locally, then act weird in CI when jobs overlap a little.

The baseline pattern I use before clicking reset

My default rule is: capture what exists first, then diff for something new. The baseline can be a message id list, newest timestamp, or both. I like both because they explain different failures.

The flow is pretty small:

  1. Create or fetch the inbox for this test run.
  2. Read current messages and store the newest timestamp plus known ids.
  3. Trigger the password reset in the browser.
  4. Poll until a message appears that is newer than the baseline and matches the expected recipient.
  5. Assert the email content still proves it belongs to this run.

That last step matters more than people think. If your app can include a run label, request id, or masked user identifier in the message body, use it. If not, at least compare timestamps and recipient carefully. Even a basic provider like tempmailso becomes much easier to test against when the suite stops treating the inbox like a black box.

A small Playwright helper for mailbox diffing

Here is the rough helper shape I use:

type Baseline = {
  seenIds: Set<string>;
  newestReceivedAt: number;
};

async function captureBaseline(inboxId: string): Promise<Baseline> {
  const messages = await mail.listMessages(inboxId);
  return {
    seenIds: new Set(messages.map((msg) => msg.id)),
    newestReceivedAt: Math.max(0, ...messages.map((msg) => msg.receivedAt)),
  };
}

async function waitForNewResetMail(inboxId: string, baseline: Baseline) {
  return mail.waitForMessage({
    inboxId,
    timeoutMs: 45_000,
    predicate: (msg) =>
      !baseline.seenIds.has(msg.id) &&
      msg.receivedAt >= baseline.newestReceivedAt &&
      msg.subject.includes("Reset your password"),
  });
}
Enter fullscreen mode Exit fullscreen mode

This is not fancy code, and that is the point. I want the helper to be obvious when somebody reads a failed run at 7 AM. You can extend it with aliases, per-test labels, or stricter body checks, but the baseline idea should stay boring and repeatable.

One small gotcha: if your provider timestamps with second-level precision, use > when you can and store ids too. Relying only on timestamps can be a bit sloppy when two messages land in the same second.

What to log when the new email never arrives

When the wait times out, I want the report to answer these questions right away:

  • What was the baseline newest timestamp?
  • How many message ids were already present?
  • Did any new messages arrive with a different subject?
  • Was the recipient alias unique to this run?
  • Did the app return success before mail dispatch actually finished?

Those details turn a vague failure into a direction. Without them, people start stretching timeouts again, which is often the wrong fix. With them, you can usually tell whether the issue was provider delay, app-side queueing, or a bad mailbox assumption. That makes triage a lot less noisy, and frankly less annoying for whoever is on call.

A short QA checklist

Before I call a reset-email test stable, I check these:

  • The inbox is unique per run or per alias group.
  • A baseline snapshot is captured before the UI action.
  • Message selection excludes ids seen before the trigger.
  • The chosen email has at least one ownership signal beyond the subject.
  • Timeout logs include the baseline summary.

If two or three of those are missing, the test can still pass, but it is not robust yet. It is just lucky enough today.

Q&A

Do I still need a fresh inbox every time?

Not always. A unique alias can work fine if your provider exposes enough metadata. I still prefer fresh inboxes for auth-critical checks because the failure story is cleaner.

Is this pattern only useful for password resets?

No. Verification emails, magic links, invite flows, and one-time codes all benefit from the same baseline approach. Reset tests simply show the pain first because the subjects are so repetitive.

Top comments (0)