Reliable user signup flows are essential for web applications, and automated testing plays a key role in ensuring they perform as expected. This article will show you how to use Playwright and testmail.app to automate end-to-end signup tests, covering everything from form submission to email verification workflows like OTPs and magic links.
Why choose Playwright for automated testing?
Playwright is a fast, reliable, and flexible automation framework built for modern web apps. It comes with a powerful feature set that makes flows like user signups easy to automate.
- Cross-browser and cross-platform: Runs on Chromium (Chrome/Edge), Firefox, and WebKit (Safari) and works the same on macOS, Linux, and Windows — no code changes needed.
- Stable, low-flakiness tests: Auto-waiting is built in, so tests fail less often compared to older tools.
- Powerful, developer-friendly API: Makes it simple to handle browser contexts, tabs, frames, user interactions, and even Shadow DOM elements.
- Great debugging tools: Playwright Inspector and Trace Viewer let you replay steps, understand failures, and fix issues quickly.
- Rich feature set for modern apps: Includes mobile device emulation, visual comparisons, network request interception, and accessibility checks.
- Multi-language support + codegen: Works with JavaScript, TypeScript, Python, Java, and C#, and can auto-generate test scripts for you.
- CI/CD ready: Integrates smoothly with GitHub Actions, GitLab, Jenkins, Azure DevOps, and more.
Why use testmail.app for email testing?
If you rely only on Playwright, you’d have to automate a real webmail UI like Gmail to read emails — a slow, brittle approach that breaks easily and limits how many inboxes you can use. testmail.app fixes this by giving you disposable, programmable inboxes you can access directly from your tests. You can capture and validate emails like confirmations, OTPs, and magic links instantly, without scraping or manual setup.
Prerequisites
Before starting, ensure you have the following:
- Node.js and npm installed: Download and install Node.js from the official website.
- testmail.app account: Sign up at testmail.app and get your API key and namespace from the dashboard.
- Playwright installed: To install Playwright globally, run:
npm init playwright@latest
npx playwright installAn example spec file will be generated under the e2e folder, where you can write your tests. Once done, you can run the tests using the following command:
npx playwright testDefining test requirements
Before writing automated signup tests, it’s important to clearly define what you want to cover. For a signup flow, key test requirements might include:
Form validation
- Required fields trigger validation errors
- Email format validation (e.g., [email protected])
- Password strength enforcement (if applicable)
Account creation
- Successful signup with valid inputs
- Proper database record creation (if checkable via API or DB)
Email delivery
- Confirmation/verification email sent to the correct address
- Email subject and body contain expected content
- OTP, magic link, or token is present and extractable
Email verification flow
- OTP or magic link completes account verification
- Expired or invalid tokens are handled gracefully
- User is redirected to the correct post-verification page
Duplicate handling
- Signing up with an existing email shows an appropriate error
- Re-requesting verification emails works as expected
Resilience and error handling
- Network failures or server errors during signup don’t crash the app
- Rate limits, captchas, or abuse protections are testable (if applicable)
Testing user sign-up: what we’ll cover
In this article, we’ll verify the following key steps in the signup flow:
- User can load the application
- User can sign up with a generated email address
- User receives a confirmation code or magic link via email
- User can enter the confirmation code or click the magic link to confirm the account
- User can log in and see the welcome screen
How to create multiple inboxes in testmail.app?
After signing up for testmail.app, access your console to get your unique namespace and API key. Use this API key to authenticate your API requests. You can also configure the API key permissions in your console to control access to one or more namespaces.
To create multiple email addresses, testmail.app lets you combine your namespace with tags. Tags are flexible strings you define on the fly — no need to register them in advance. This makes it easy to generate unique email addresses and mailboxes for different tests or use cases.
Email address format:
testmail.app receives emails at {namespace}.{tag}@inbox.testmail.app.
For example, if your namespace is acmeinc, sending emails to [email protected] or [email protected] will route them to the acmeinc namespace, with the tags hello and hey respectively.
What makes this even more powerful is that tags are indexed using n-grams, allowing for highly efficient queries. You can nest tags like category.subcategory and then query by prefix:
tag_prefix:"category"→ returns all emails under that categorytag_prefix:"category.subcategory"→ narrows it further to the subcategory
This approach helps you group and organize inboxes, avoid naming conflicts, and reduce reliance on advanced filters (which can be less performant).
testmail.app also offers unlimited custom namespaces. This is especially useful for teams managing multiple projects, clients, or testing environments — you can segment everything cleanly without running into limits or overlap.
Fetching your emails: testmail.app provides two ways to retrieve your test emails: a simple JSON API and a more flexible GraphQL API (check the documentation for a feature comparison).
Trigger signup
With Playwright set up, the next step is to navigate to your application’s signup page and simulate a user signup. This involves loading the page, filling in the email field with a test address, and submitting the form to trigger the confirmation email. Here’s an example of how to do that in Playwright:
const { test, expect } = require('@playwright/test');
test.beforeEach(async ({ page }) => {
// Navigate to your app's signup page
await page.goto('https://yourapp.com/signup');
// Fill in the email field with the test email address
const TESTEMAIL = '[email protected]'; // Replace with your test email
await page.fill('input[name="email"]', TESTEMAIL);
// Submit the signup form to trigger the email
await page.click('button[type="submit"]');
});Verifying signups: testing OTPs and magic links
Let’s verify that the signup email is successfully delivered to the test inbox. We’ll use the Testmail JSON API to query the inbox and retrieve the email. To avoid repeatedly polling the inbox, we recommend adding the &livequery=true parameter — this makes the API wait until at least one matching email arrives before responding.
For OTP flows, we’ll extract the code from the email body and submit it in the app to confirm the account. For magic link flows, we’ll extract the URL from the email and navigate to it in a new browser context. In both cases, we’ll also validate that the sender, subject, and content match expected values before proceeding.
OTP flow
const { test, expect } = require('@playwright/test');
const axios = require('axios');
test('complete signup with OTP', async ({ page }) => {
const API_KEY = 'YOUR_APIKEY'; // ← Replace with your Testmail.app API key
const NAMESPACE = 'YOUR_NAMESPACE'; // ← Replace with your Testmail.app namespace
// Query the Testmail inbox
const res = await axios.get('https://api.testmail.app/api/json', {
params: {
apikey: API_KEY,
namespace: NAMESPACE,
livequery: 'true',
tag: 'signup' // ← Optional: replace or remove tag depending on your setup
}
});
const email = res.data.emails[0];
// Verify sender, subject, and content
expect(email.from).toContain('[email protected]'); // ← Replace with your app’s sender email
expect(email.subject).toContain('Your Signup Code'); // ← Replace with your actual signup email subject
expect(email.text).toContain('Your OTP code is'); // ← Adjust to match your email text
// Extract OTP (assumes 6-digit code)
const otpCode = email.text.match(/\d{6}/)[0];
// Submit OTP in the app
await page.fill('input[name="otp"]', otpCode); // ← Replace selector if needed
await page.click('button[type="submit"]'); // ← Replace selector if needed
// Verify welcome page
await expect(page).toHaveURL(/.*welcome/); // ← Adjust to match your welcome page URL pattern
});Magic link flow
const { test, expect } = require('@playwright/test');
const axios = require('axios');
test('complete signup with magic link', async ({ page, context }) => {
const API_KEY = 'YOUR_APIKEY'; // ← Replace with your Testmail.app API key
const NAMESPACE = 'YOUR_NAMESPACE'; // ← Replace with your Testmail.app namespace
// Query the Testmail inbox
const res = await axios.get('https://api.testmail.app/api/json', {
params: {
apikey: API_KEY,
namespace: NAMESPACE,
livequery: 'true',
tag: 'signup' // ← Optional: replace or remove tag depending on your setup
}
});
const email = res.data.emails[0];
// Verify sender, subject, and content
expect(email.from).toContain('[email protected]'); // ← Replace with your app’s sender email
expect(email.subject).toContain('Confirm Your Account'); // ← Replace with your actual signup email subject
expect(email.text).toContain('Click the link below to confirm'); // ← Adjust to match your email text
// Extract magic link (looks for first URL in the text)
const magicLink = email.text.match(/https?:\/\/\S+/)[0];
// Open magic link in new page
const magicLinkPage = await context.newPage();
await magicLinkPage.goto(magicLink);
// Verify welcome page
await expect(magicLinkPage).toHaveURL(/.*welcome/); // ← Adjust to match your welcome page URL pattern
});Optional: Monitor spam scores during testing
While spam testing is usually handled outside of automated signup tests, it can be valuable to monitor spam scores during signup flows, especially when using new email templates or sender domains.
With testmail.app, you can enable spam reports by adding &spam_report=true to your API calls:
const res = await axios.get('https://api.testmail.app/api/json', {
params: {
apikey: 'YOUR_APIKEY', // replace with your API key
namespace: 'YOUR_NAMESPACE', // replace with your namespace
livequery: 'true',
spam_report: 'true'
}
});
const email = res.data.emails[0];
console.log('Spam score:', email.spam_score);
console.log('Spam report:', email.spam_report);testmail.app uses SpamAssassin, the most widely used open-source spam filter, to generate these reports. Keep in mind: spam scores aren’t universal — mailbox providers often run custom rule sets and apply different thresholds.
testmail.app’s configuration includes commonly enabled rules and uses the default threshold of 5 points. A high spam score suggests a higher likelihood of hitting spam folders, but a low score doesn’t guarantee inbox delivery. Here’s the practical takeaway:
- Spam score below 5 → usually no action needed
- Spam score 5 or above → review the
spam_reportand fix the rules causing the biggest hits - Negative scores → a positive signal (yes, negative is good!)
With Playwright handling the browser and testmail.app handling the inbox, you get full visibility into how your signup flow behaves in the real world. It’s a reliable setup you can reuse across new features, CI pipelines, and regression tests to keep your onboarding experience stable. Once everything is in place, maintaining a dependable signup flow becomes almost effortless.