Testing Password Reset Emails End-to-End

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.

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), or let the app send through its normal provider to an address on a domain whose MX points at the sandbox.

The Test

inboxOf and waitForMessage are the polling helpers from the CI guide.

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 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.
  • Or skip polling. Subscribe over SSE and resolve when a Message event for the account arrives.