MailSlurp logo

Test email and SMS with Playwright

Documentation navigation
Search documentation

Build a complete Playwright email verification test with a private inbox, matching waits, OTP extraction, final login assertions, and cleanup.

View MarkdownAgent setup

Use Playwright to drive your application and MailSlurp to receive the email or SMS it sends. The standard MailSlurp SDK runs in the test process; no separate Playwright plugin is required.

Info: Learn the underlying techniques. The integration testing guide explains the core objects and recipes. The wait-for guide explains message matching, timeouts, and unread state.

Install and configure

In an existing Node.js Playwright project, install the MailSlurp SDK:

npm install --save-dev mailslurp-client

If you are starting a new Playwright project, install the runner and its browser first:

npm install --save-dev @playwright/test
npx playwright install chromium

Set MAILSLURP_API_KEY in your environment or CI secret settings using a key from the MailSlurp dashboard. The test below reads it through process.env; it never passes the key to the application page.

Playwright for other languages uses the corresponding Java, Python, or C# SDK. The C# Playwright and NUnit SMS example shows that combination.

Complete email verification test

Save this as tests/email-verification.spec.ts. It exercises the MailSlurp playground: create an inbox, sign up, receive the verification email, extract a six-digit code, confirm the user, and sign in.

import { test, expect } from '@playwright/test';
import {
  MailSlurp,
  MatchOptionFieldEnum,
  MatchOptionShouldEnum,
} from 'mailslurp-client';

test('a new user can verify their email and sign in', async ({ page }, testInfo) => {
  test.setTimeout(180_000);
  const apiKey = process.env.MAILSLURP_API_KEY;
  if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');
  const mailslurp = new MailSlurp({ apiKey });
  const inbox = await mailslurp.createInboxWithOptions({ expiresIn: 600_000 });
  const password = 'Test-password-42!';

  try {
    await page.goto('https://playground.mailslurp.com');
    await page.locator('[data-test="sign-in-create-account-link"]').click();
    await page.locator('input[name=email]').fill(inbox.emailAddress);
    await page.locator('input[name=password]').fill(password);

    const since = new Date();
    await page.locator('[data-test="sign-up-create-account-button"]').click();

    const email = await mailslurp.waitController.waitForMatchingFirstEmail({
      inboxId: inbox.id,
      since,
      timeout: 60_000,
      unreadOnly: true,
      matchOptions: {
        matches: [{
          field: MatchOptionFieldEnum.SUBJECT,
          should: MatchOptionShouldEnum.CONTAIN,
          value: 'Please confirm your email address',
        }],
      },
    });
    await testInfo.attach('mail-message', {
      body: JSON.stringify({ inboxId: inbox.id, emailId: email.id }),
      contentType: 'application/json',
    });
    expect(email.to).toContain(inbox.emailAddress);
    const match = /verification code is\s+(\d{6})\b/i.exec(email.body ?? '');
    if (!match) throw new Error(`No verification code in email ${email.id}`);
    const code = match[1];
    expect(code).toMatch(/^\d{6}$/);

    await page.locator('[data-test="confirm-sign-up-confirmation-code-input"]').fill(code);
    await page.locator('[data-test="confirm-sign-up-confirm-button"]').click();
    await page.locator('[data-test="username-input"]').fill(inbox.emailAddress);
    await page.locator('[data-test="sign-in-password-input"]').fill(password);
    await page.locator('[data-test="sign-in-sign-in-button"]').click();
    await expect(page.locator('[data-test="greetings-nav"]')).toBeVisible();
  } finally {
    // Expiry also removes the inbox if the runner is interrupted.
    try {
      await mailslurp.inboxController.deleteInbox({ inboxId: inbox.id });
    } catch {
      // Keep cleanup diagnostics separate from the original test failure.
      await testInfo.attach('mail-cleanup', {
        body: `Inbox cleanup failed for ${inbox.id}; its configured expiry remains in place.`,
        contentType: 'text/plain',
      });
    }
  }
});

Run the test with:

npx playwright test tests/email-verification.spec.ts --workers=1

Each attempt creates its own inbox, so this resource pattern also supports parallel workers. When adapting it to your app, replace the playground URL, selectors, expected subject, code format, and final application assertion. The browser flow stays within one test, so it does not depend on ordered tests or shared state.

For a password reset or magic-link flow, wait for the appropriate subject and extract the links from that full email. In the test above, replace the OTP extraction and form submission with your application's link flow. This snippet assumes mailslurp, email, and page are in scope:

const { links } = await mailslurp.emailController.getEmailLinks({ emailId: email.id });
const candidates = links.filter((link) => {
  const url = new URL(link);
  return url.origin === 'https://app.example.com' && url.pathname === '/auth/verify';
});
expect(candidates).toHaveLength(1);
await page.goto(candidates[0]);
await expect(page.getByRole('heading', { name: 'Your account' })).toBeVisible();

Replace the origin, path, and heading with your actual test application. Keep the browser context that requested the link when the authentication system requires matching cookies or state. Avoid first navigating to a link with a separate HTTP request if that would consume a single-use token.

Info: Test magic links end to end. The NextAuth example and magic-link guide demonstrate requesting an email and following the delivered link in Playwright.

Receive SMS OTPs

Reserve a provisioned MailSlurp phone number for the test and load it by ID. Enter its dialable phoneNumber into your application's phone form. Capture a timestamp before submitting the form, then call waitForLatestSms with the phone ID, since, an explicit timeout, and unreadOnly: true.

Extract the code as a string, submit it in Playwright, and assert authenticated access. If several message types can arrive, use body/sender filters with waitForSms. Do not let parallel workers select the same arbitrary first number from an account list.

Info: Copy the SMS workflow. The SMS recipe includes the SDK calls. The Playwright SMS project supplies a full browser flow. For authenticator challenges, use TOTP devices.

Inspect content, attachments, and rendering

Use the received email's ID to query HTML or extract structured values, download attachments, or obtain preview URLs. Open an HTML preview in a separate page when you want to inspect its visible content without navigating away from the application session.

A browser preview checks the stored HTML. For Gmail, Outlook, Apple Mail, and other real-client output, use device previews and inspect the requested targets and screenshot results.

For variable email layouts, the Playwright AI example waits for a scoped message, extracts an OTP against a schema, and checks extraction evidence before submitting it.

Fixtures, retries, and CI

Move inbox creation and teardown into a test-scoped fixture when several tests need the same setup. Each test and retry should get a fresh inbox; keep the complete signup and confirmation journey in one test. A worker-scoped phone fixture can reserve a persistent number, provided scenarios on that worker run without competing for messages.

Set the test timeout to cover setup, message waits, browser actions, and cleanup. Configure the SDK's HTTP transport timeout separately when needed. Keep since immediately before the application's send action, use meaningful subject matching, and reuse the full email object after reading it.

On failure, collect inbox/message IDs and the expected filter alongside Playwright traces. Keep OTPs, full bodies, and authentication links out of broadly shared logs. Clean up your application's test user separately if its lifecycle requires it; deleting the inbox does not delete that user.

Info: Diagnose a failed wait. The wait troubleshooting table separates stale messages, read-state changes, mismatched filters, HTTP timeouts, and missing sends.

Example projects and video walkthrough

Browse the Playwright projects for email, SMS, and recorded browser flows. Use the complete test above as the starting pattern for isolation, matching, and cleanup.

Watch the email testing walkthrough alongside the code:

Open video on YouTube