Article featured image
End-to-end password reset testing with Selenium
6min readLast updated: February 27, 2026

Password reset flows are a critical part of user experience. Many helpdesk and support teams report that a notable share of authentication-related tickets are tied to forgotten passwords or account recovery, underlining how often users interact with these flows.

Because this path relies on email delivery and unique reset links, it presents specific challenges for automation. Real email services can block or rate-limit automated access, and temporary email providers may be unreliable in CI environments. Tools like testmail.app make it feasible to capture and assert password reset emails programmatically as part of a reliable end-to-end test suite.

What a password reset email should include

A password reset email is often opened when a user is locked out and trying to get back into their account quickly. The message needs to be clear, trustworthy, and easy to act on.

Example of a password reset email

A clear subject line: The subject should immediately explain why the email was sent, such as a password reset request, so it isn’t confused with alerts or marketing emails.

A single, obvious reset action: One prominent reset link or button that takes the user directly to the password reset page, without additional or competing actions.

Brief context and reassurance: A short explanation confirming that a password reset was requested and what the user should do next.

Guidance if the request wasn’t intentional: Clear instructions that the email can be ignored if the user didn’t request the reset, along with a way to contact support if they’re concerned about account security.

Clear sender identity: Recognizable branding and a trustworthy sender name so users know the email is legitimate.

Information about link validity: If the reset link expires or can only be used once, users expect this to be clearly stated.

We’ve already covered general best practices for transactional emails in this article, so we won’t go deep into that here. Let’s move on to how to test the password-reset flow in practice.

Is selenium the right tool for you?

Password reset flows span both your application and email delivery, so the testing tool needs to handle more than just form submissions. Selenium is a good fit when you need true end-to-end coverage. It can drive a real browser through the full flow, from requesting a password reset to opening the email link and completing the reset. It works well when tests are focused on user-visible behavior rather than internal implementation details. This makes it suitable for validating redirects, tokenized URLs, form validation, and success states exactly as users experience them.

Selenium is also a practical choice for teams running tests in CI environments. It supports headless execution, integrates with most CI providers, and offers stable cross-browser support when compatibility matters. While Selenium isn’t the only option for UI testing, it’s a strong choice when your goal is to automate realistic, browser-based password reset flows that include email delivery.

What you need before you start

Before automating the password reset flow, make sure the following are in place.

Selenium set up locally or in CI
A working Selenium setup with a supported browser (such as Chrome or Firefox) and the appropriate driver configured. The same setup can run in headless mode when executing tests in CI environments.

If you’re new to Selenium, the official docs are a good starting point:
https://www.selenium.dev/documentation/

A testmail.app account
We'll use testmail.app to receive password reset emails in a controlled inbox and query them via API. This makes it easy to wait for the email and extract the reset link during tests.

Access to your application's password reset flow
A non-production environment that exposes the complete password reset flow, from requesting a reset to setting a new password, using test users that can be created or reused safely.

Automating the password reset flow with Selenium + testmail.app

Here’s the high-level flow your automated test covers:

  1. Request password reset: The test submits a password reset request through the application’s UI using a dedicated test email address.
  2. Receive email in testmail.app: The reset email is captured in a testmail.app inbox, allowing your test to access it programmatically.
  3. Extract reset link: The test parses the email content to extract the password reset link, just as a user would click it.
  4. Open link and set new password: Using Selenium, the test opens the reset link in a browser and completes the password update.
  5. Verify successful login: Finally, the test confirms that the new password works by logging in, ensuring the entire flow from request to completion is functional.
/**
 * Required environment variables:
 * TESTMAIL_APIKEY
 * TESTMAIL_NAMESPACE
 * APP_BASE_URL
 */

require('chromedriver');
require('dotenv').config();

const { Builder, By, until } = require('selenium-webdriver');
const { GraphQLClient } = require('@testmail.app/graphql-request');
const Chance = require('chance');
const assert = require('assert');

// Initialize Testmail client
const testmailClient = new GraphQLClient(
  'https://api.testmail.app/api/graphql',
  {
    headers: {
      Authorization: `Bearer ${process.env.TESTMAIL_APIKEY}`,
    },
  }
);

// Generate a unique inbox tag per test run
const chance = new Chance();
const TAG = chance.string({
  length: 12,
  pool: 'abcdefghijklmnopqrstuvwxyz0123456789',
});

// Record timestamp so we only fetch emails from this test run
const startTimestamp = Date.now();

describe('Password reset flow', function () {
  let driver;
  let resetLink;

  before(async function () {
    driver = await new Builder().forBrowser('chrome').build();

    // Navigate to password reset page
    await driver.get(`${process.env.APP_BASE_URL}/forgot-password`);

    // Submit password reset request using a Testmail inbox
    const emailInput = await driver.findElement(By.name('email'));
    await emailInput.sendKeys(
      `user+${TAG}@${process.env.TESTMAIL_NAMESPACE}.testmail.app`
    );

    await driver.findElement(By.css('button[type="submit"]')).click();

     // Wait for the success message to appear
    await driver.wait(
      until.elementLocated(By.css('.message.success')),
      10000
    );
  });

  it('Fetch password reset email from Testmail', async function () {
    const response = await testmailClient.request(
      `{
        inbox(
          namespace: "${process.env.TESTMAIL_NAMESPACE}"
          tag: "${TAG}"
          timestamp_from: ${startTimestamp}
          livequery: true
        ) {
          result
          count
          emails {
            subject
            html
          }
        }
      }`
    );

    const inbox = response.inbox;

    assert.equal(inbox.result, 'success');
    assert.equal(inbox.count, 1);
    assert.ok(inbox.emails[0].subject.includes('Password Reset'));

    // Extract reset link from email HTML
    const match = inbox.emails[0].html.match(
      /(https?:\/\/[^\s"]*reset[^\s"]*)/
    );
    assert.ok(match, 'Reset link not found in email');

    resetLink = match[0];
  });

  it('Complete password reset using the reset link', async function () {
    await driver.get(resetLink);

    await driver.wait(until.elementLocated(By.name('password')), 10000);

    await driver.findElement(By.name('password')).sendKeys('NewSecurePass123!');
    await driver
      .findElement(By.name('confirmPassword'))
      .sendKeys('NewSecurePass123!');

    await driver.findElement(By.css('button[type="submit"]')).click();

    // Assert successful reset (URL or success message)
    await driver.wait(until.urlContains('login'), 10000);
  });

  after(async function () {
    if (driver) {
      await driver.quit();
    }
  });
});

When the test starts, it generates a unique tag and combines it with your testmail.app namespace to create a dedicated inbox for this run. This keeps password reset emails isolated and avoids interference from other tests or previous executions.

After submitting the reset request through the UI, the test queries the Testmail API using livequery:true. Instead of relying on fixed waits, this lets the test wait for the email to arrive asynchronously. The timestamp_from filter ensures only emails sent after the test began are considered, which is especially useful when inboxes are reused.

Once the email is received, the reset link is extracted directly from the HTML content. The test then opens that link in a real browser session and completes the password update through the UI. This confirms that the entire password reset flow works end-to-end, from requesting the reset to successfully setting a new password, as a real user would experience it.

Additional password reset scenarios worth testing

Expired reset link: If a user tries to use a password reset link after it has expired, the app should show a clear error and allow them to request a new link. Testing this ensures users don’t get stuck or confused when links expire.

Multiple reset requests: When a user requests multiple password resets in a short period, only the most recent link should be valid. This prevents older links from being used and confirms that the system handles concurrent requests correctly.

Non-existent email address: Submitting a password reset request for an email that doesn’t exist should return a generic response. This avoids revealing which accounts are registered and protects against user enumeration attacks.

Email delivery delay: Sometimes emails take longer to arrive due to network or service delays. Ensuring your test waits dynamically for the email helps prevent flaky test failures and mirrors real-world conditions.

Password policy enforcement: Attempting to set a password that doesn’t meet the application’s requirements should trigger a clear validation message. This helps maintain security while giving users understandable guidance.

Already-used reset link: After a password has been successfully reset, the same link should no longer work. Testing this ensures links are single-use and the system handles repeated attempts gracefully.

Subscribe to blog

Stay updated with our latest insights and curated articles delivered straight to your inbox.