---
title: "Testing Password Reset Emails End-to-End"
description: "A Playwright test that triggers a reset, polls the API for the email, extracts the link, and finishes the flow. Same pattern for signup confirmations."
canonical_url: "https://smtp.dev/docs/guides/e2e/"
last_updated: "2026-07-16T01:19:47.790Z"
---

The reset email is the step most E2E suites skip or mock. With the app's outbound mail landing in the sandbox, the test follows the real link instead.

Two ways to get the mail there: point the app's SMTP transport at `send.smtp.dev` with a sandbox account's credentials (shown in the [CI guide](/docs/guides/ci)), or let the app send through its normal provider to an address on a [domain whose MX points at the sandbox](/docs/setup/domain).

## The Test

`inboxOf` and `waitForMessage` are the polling helpers from the [CI guide](/docs/guides/ci).

```ts
import { expect, test } from '@playwright/test'

test('password reset', async ({ page }) => {
  const address = 'reset-user@example.com'

  await page.goto('/forgot-password')
  await page.getByLabel('Email').fill(address)
  await page.getByRole('button', { name: 'Send reset link' }).click()

  const message = await waitForMessage(
    await inboxOf(address),
    m => m.subject === 'Reset your password',
  )

  const body = message.text || (message.html ?? []).join('')
  const link = body.match(/https?:\/\/\S*reset\S*/)?.[0]
  expect(link).toBeTruthy()

  await page.goto(link)
  await page.getByLabel('New password').fill('correct-horse-battery')
  await page.getByRole('button', { name: 'Set password' }).click()
  await expect(page).toHaveURL(/signin/)
})
```

The message's `text` field is the plain text part; `html` is an array of HTML parts. If your template only wraps the URL in a button, match the `href` instead: `body.match(/href="([^"]+)"/)?.[1]`.

## Keeping It Stable

- **One address per worker.** `reset+worker1@example.com` [delivers to](/docs/features/account#plus-sign-aliasing) `reset@example.com` with the alias intact in `to`, so parallel workers never read each other's mail. Match on recipient and subject, not arrival order.
- **Poll tight.** One-second intervals across a few workers sit nowhere near the [4096 requests per minute limit](/docs/api#rate-limiting).
- **Or skip polling.** [Subscribe over SSE](/docs/api#real-time-updates) and resolve when a `Message` event for the account arrives.
