Learn how to create end-to-end integration tests for email verification flows using Playwright and DevInbox. This tutorial will guide you through building a simple web application with email verification and testing it with Playwright, ensuring your email functionality works correctly in automated tests.
Prerequisites
Before we begin, make sure you have the following:
- Node.js 18+ installed on your system
- npm or yarn package manager
- A DevInbox account - Sign up for free if you don't have one
- Your DevInbox API Key - You can find it in your DevInbox dashboard
Create the Email Template
Before running the tests, you need to create a template named
subscribe_template
in your DevInbox dashboard. The template should use Mustache syntax to extract the verification code.
Subject:
Thank you for subscribing to dummy news
Body (HTML):
<html>
<body>
<p>Thank you for subscribing to dummy news. Your verification code is: {{ verification_code }}</p>
</body>
</html>Step 1: Create a New Web Application
Let's start by creating a new Node.js project with TypeScript support.
# Create a new directory for the project
mkdir email-verification-app
cd email-verification-app
# Initialize npm project
npm init -y
# Install TypeScript and dependencies
npm install --save-dev typescript @types/node ts-node nodemon
npm install express @types/express nodemailer
# Create TypeScript configuration
npx tsc --initUpdate your package.json to include scripts for running the application:
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}Step 2: Build the Email Subscription Form
Create a simple HTML page with an email input field and a submit button. We'll use Express to serve this page.
First, create the directory structure:
# Linux/Mac
mkdir -p src public
# PowerShell (Windows)
New-Item -ItemType Directory -Force -Path src,publicCreate the HTML form page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Subscription</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="email"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #000; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #333; }
.message { margin-top: 20px; padding: 10px; border-radius: 4px; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>Subscribe to Dummy News</h1>
<form id="subscribeForm">
<div class="form-group">
<label for="email">Email Address:</label>
<input type="email" id="email" name="email" required>
</div>
<button type="submit">Subscribe</button>
</form>
<div id="message"></div>
<script>
document.getElementById('subscribeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const messageDiv = document.getElementById('message');
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
if (response.ok) {
messageDiv.className = 'message success';
messageDiv.textContent = 'Verification email sent! Please check your inbox.';
} else {
throw new Error(data.error || 'Failed to send email');
}
} catch (error) {
messageDiv.className = 'message error';
messageDiv.textContent = 'Error: ' + error.message;
}
});
</script>
</body>
</html>Step 3: Configure SMTP Settings
Create the Express server with configurable SMTP settings. The server will accept SMTP configuration via environment variables, making it easy to configure from tests.
import express from 'express';
import nodemailer from 'nodemailer';
import path from 'path';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.static('public'));
// SMTP configuration from environment variables
// These can be set by the test to configure the SMTP server
const smtpConfig = {
host: process.env.SMTP_HOST || 'localhost',
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: process.env.SMTP_USER ? {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
} : undefined
};
// Verification code - can be set via environment variable for testing
const VERIFICATION_CODE = process.env.VERIFICATION_CODE ||
Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const transporter = nodemailer.createTransport(smtpConfig);
// Subscribe endpoint
app.post('/api/subscribe', async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
// Send verification email
await transporter.sendMail({
from: 'noreply@example.com',
to: email,
subject: 'Thank you for subscribing to dummy news',
html: `<html>
<body>
<p>Thank you for subscribing to dummy news. Your verification code is: ${VERIFICATION_CODE}</p>
</body>
</html>`
});
res.json({ success: true, message: 'Verification email sent' });
} catch (error: any) {
console.error('Error sending email:', error);
res.status(500).json({ error: 'Failed to send email' });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`SMTP Host: ${smtpConfig.host}:${smtpConfig.port}`);
console.log(`Verification Code: ${VERIFICATION_CODE}`);
});Update your tsconfig.json to enable ES modules:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}Add "type": "module" to your package.json:
{
"type": "module",
"scripts": {
"dev": "nodemon --exec ts-node --esm src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}Step 4: Test the Application Manually
Before writing the integration test, let's verify the application works. Start the server:
npm run devOpen http://localhost:3000 in your browser and test the form. For now, you'll need to configure a real SMTP server or use a service like Gmail for testing.
Step 5: Set Up Playwright for Testing
Now let's install Playwright and set up the test framework.
# Install Playwright and test dependencies
npm install --save-dev @playwright/test @types/node
# Install Playwright browsers
npx playwright installCreate a Playwright configuration file:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
testMatch: '**/*.spec.ts',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});Step 6: Install DevInbox Node.js SDK
Install the DevInbox client API package to interact with DevInbox in your tests.
# Install DevInbox SDK
npm install --save-dev @dev-inbox/client-apiFor more information about the DevInbox Node.js SDK, visit the GitHub repository.
Step 7: Create the Integration Test
Now let's create a comprehensive integration test that validates the entire email verification flow. The test follows this sequence:
- 1Create a DevInbox Mailbox
Before each test run, we create a fresh mailbox using the DevInbox API. This ensures test isolation and prevents interference between test runs. The mailbox provides a unique email address that will receive the verification email.
- 2Configure and Start the Web Application
The test starts your Express server with SMTP settings configured to use DevInbox's SMTP server. Environment variables are set to route all outgoing emails through DevInbox, ensuring they're captured for verification.
- 3Generate a Verification Code
A random 6-digit verification code is generated and passed to the server via environment variables. This code will be included in the email sent by your application, allowing us to verify end-to-end that the correct code is delivered.
- 4Interact with the Form Using Playwright
Playwright opens a browser, navigates to your subscription page, fills in the email field with the DevInbox mailbox address, and submits the form. This simulates real user interaction and tests the complete user-facing flow.
- 5Verify Email Delivery and Content
Using the DevInbox API, we retrieve the received email and parse it using the template you created earlier. The test extracts the verification code from the email body and asserts that it matches the expected code, confirming the entire flow works correctly.
First, create the tests directory:
mkdir testsCreate the integration test file:
import { test, expect } from '@playwright/test';
import { Configuration, MailboxesApi, MessagesApi } from '@dev-inbox/client-api';
import { spawn, ChildProcess } from 'child_process';
import { setTimeout as delay } from 'timers/promises';
// Configure DevInbox API client
const apiKey = process.env.DEVINBOX_API_KEY;
if (!apiKey) {
throw new Error('DEVINBOX_API_KEY environment variable is required');
}
const configuration = new Configuration({
basePath: process.env.DEVINBOX_API_URL || 'https://api.devinbox.io',
baseOptions: {
headers: {
'X-API-Key': apiKey
}
}
});
const mailboxesApi = new MailboxesApi(configuration);
const messagesApi = new MessagesApi(configuration);
let serverProcess: ChildProcess | null = null;
// Helper function to start the server
async function startServer(smtpHost: string, smtpPort: number, smtpUser: string, smtpPass: string, verificationCode: string): Promise<ChildProcess> {
return new Promise((resolve, reject) => {
const env = {
...process.env,
'SMTP_HOST': smtpHost,
'SMTP_PORT': smtpPort.toString(),
'SMTP_USER': smtpUser,
'SMTP_PASS': smtpPass,
'VERIFICATION_CODE': verificationCode,
'PORT': '3000'
};
const proc = spawn('npm', ['run', 'dev'], {
env: env,
shell: true,
stdio: 'pipe'
});
let output = '';
proc.stdout?.on('data', (data) => {
output += data.toString();
if (output.includes('Server running')) {
resolve(proc);
}
});
proc.stderr?.on('data', (data) => {
console.error('Server error:', data.toString());
});
proc.on('error', reject);
// Timeout after 10 seconds
globalThis.setTimeout(() => {
if (!output.includes('Server running')) {
proc.kill();
reject(new Error('Server failed to start within timeout'));
}
}, 10000);
});
}
// Helper function to stop the server
async function stopServer(proc: ChildProcess) {
if (proc) {
proc.kill();
await delay(1000); // Give it time to shut down
}
}
test.beforeEach(async () => {
// Create a new DevInbox mailbox for each test
const mailboxResponse = await mailboxesApi.createMailbox({});
const mailbox = mailboxResponse.data;
const mailboxEmail = `${mailbox.key}@devinbox.io`;
// Generate a random verification code
const verificationCode = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
// Get SMTP settings from DevInbox
// DevInbox SMTP server configuration
const smtpHost = 'smtp.devinbox.io';
const smtpPort = 587;
// Use mailbox key as username and mailbox password from createMailbox response
const smtpUser = mailbox.key;
const smtpPass = mailbox.password;
// Start the server with SMTP configuration
serverProcess = await startServer(smtpHost, smtpPort, smtpUser, smtpPass, verificationCode);
// Store mailbox info in test context
test.info().annotations.push({
type: 'mailbox',
description: `Mailbox: ${mailbox.key}, Code: ${verificationCode}`
});
// Store in a way accessible to the test
(global as any).__testMailbox = { mailbox, mailboxEmail, verificationCode };
});
test.afterEach(async () => {
if (serverProcess) {
await stopServer(serverProcess);
serverProcess = null;
}
});
test('should send verification email and verify code', async ({ page }) => {
const { mailbox, mailboxEmail, verificationCode } = (global as any).__testMailbox;
// Navigate to the subscription page
await page.goto('/');
// Fill in the email field with the DevInbox mailbox email
await page.fill('input[type="email"]', mailboxEmail);
// Click the submit button
await page.click('button[type="submit"]');
// Wait for success message
await page.waitForSelector('.message.success', { timeout: 5000 });
// Wait a moment for the email to be delivered
await delay(2000);
// Verify the email was received using DevInbox API
const parsedResponse = await messagesApi.getSingleMessageWithTemplate(
mailbox.key,
'subscribe_template'
);
const parsedMessage = parsedResponse.data;
// Assert that the message was received
expect(parsedMessage).not.toBeNull();
expect(parsedMessage.body).not.toBeNull();
// Extract the verification code from the parsed message
const bodyDict = parsedMessage.body as Record<string, string>;
const receivedCode = bodyDict['verification_code'];
// Assert that the verification code matches
expect(receivedCode).toBe(verificationCode);
console.log(`✅ Verification code matched! Expected: ${verificationCode}, Received: ${receivedCode}`);
});SMTP Configuration
The test uses DevInbox's SMTP server with the following settings:
- Host:
smtp.devinbox.io - Port:
587(STARTTLS) - Username: Mailbox key (from the created mailbox)
- Password: Mailbox password (from the created mailbox)
The test automatically configures these settings when starting the server.
Step 8: Run the Integration Test
Now that your test is set up, let's configure the test scripts and execute the integration test to verify everything works end-to-end.
Configure Test Scripts
First, add test scripts to your package.json to make running tests convenient:
{
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui"
}
}
The test script runs tests in headless mode, perfect for CI/CD pipelines. The test:ui script opens Playwright's interactive UI, which is great for debugging and watching tests run in real-time.
Set Up Environment Variables
Before running the test, you need to set your DevInbox API key as an environment variable. This allows the test to authenticate with the DevInbox API:
# On Windows (PowerShell)
$env:DEVINBOX_API_KEY="your-api-key-here"
npm test
# On Linux/Mac
export DEVINBOX_API_KEY="your-api-key-here"
npm testTroubleshooting
If you encounter Error: No tests found when running npm test, verify the following:
- The
testsdirectory exists in your project root - The test file
tests/email-verification.spec.tshas been created - The
playwright.config.tsfile is in your project root - You've installed all dependencies:
npm install
If the issue persists, try running npx playwright test --list to see if Playwright can detect your test files.
Understanding the Test Execution
When you run the test, Playwright orchestrates the entire flow automatically. Here's what happens behind the scenes:
- •
Test Setup Phase: A new DevInbox mailbox is created via the API, and your Express server starts with SMTP configured to route emails through DevInbox.
- •
Browser Automation: Playwright launches a browser, navigates to your application, and interacts with the subscription form just like a real user would.
- •
Email Verification: The test waits for email delivery, then uses the DevInbox API to retrieve and parse the received email, extracting the verification code.
- •
Assertions: The extracted verification code is compared against the expected value, ensuring the entire email flow works correctly.
- •
Cleanup: The server process is terminated, and the test completes. DevInbox automatically manages mailbox cleanup.
Expected Test Output
When the test passes, you should see output indicating:
- Successful mailbox creation
- Server startup confirmation
- Browser navigation and form submission
- Email retrieval and parsing success
- A console log showing the matched verification code
If any step fails, Playwright will provide detailed error messages and screenshots to help diagnose the issue.
Conclusion
Congratulations! You've successfully built a complete integration test that validates your email verification flow from end to end. By combining Playwright's powerful browser automation with DevInbox's email testing capabilities, you now have a robust testing solution that verifies both the user-facing interface and the email delivery system.
Key Benefits of This Approach
This testing strategy offers several significant advantages over traditional email testing methods:
- Complete Test Isolation
Each test run creates a fresh, dedicated mailbox, ensuring that tests never interfere with each other. This eliminates flaky tests caused by shared state or leftover data from previous runs.
- Zero Manual Cleanup
DevInbox automatically manages the lifecycle of mailboxes and messages. No need to manually delete test emails or clean up mailboxes between test runs—everything is handled automatically.
- Structured Email Validation
Using templates, you can verify that emails match expected formats and extract structured data reliably. This goes beyond simple string matching—you're validating the actual structure and content of your emails.
- CI/CD Integration Ready
These tests run reliably in headless mode, making them perfect for continuous integration pipelines. They execute quickly, don't require external dependencies, and provide consistent results across different environments.
- End-to-End Verification
Unlike unit tests that mock email sending, these integration tests verify the complete flow: form submission, SMTP delivery, email reception, and content validation—all in one automated test.
Extending This Pattern
The foundation you've built can be extended to test a wide variety of email-based workflows:
- •
Password Reset Flows: Test that reset links are generated correctly and contain valid tokens
- •
Order Confirmations: Verify that purchase confirmations include correct order details, totals, and shipping information
- •
Multi-Step Verification: Test complex flows requiring multiple emails, such as two-factor authentication or account setup sequences
- •
Notification Systems: Validate that users receive timely notifications for important events like security alerts or system updates
- •
Email Templates: Ensure that different user segments receive appropriately personalized content
By following this pattern, you can build a comprehensive test suite that gives you confidence in your email functionality, catching issues before they reach production and ensuring a reliable user experience.
Ready to test your email flows?
Get 1,500 operations for free every month. Unlimited volatile sandboxes included. No credit card required.
Start Testing Free