Tutorial February 4, 2026 15 min read

Integration Testing with
Playwright and DevInbox

Ado
Ado
Engineering Team

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.

Terminal
# 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 --init

Update your package.json to include scripts for running the application:

package.json
{
  "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:

Terminal
# Linux/Mac
mkdir -p src public

# PowerShell (Windows)
New-Item -ItemType Directory -Force -Path src,public

Create the HTML form page:

public/index.html
<!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.

src/server.ts
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:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true
  }
}

Add "type": "module" to your package.json:

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:

Terminal
npm run dev

Open 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.

Terminal
# Install Playwright and test dependencies
npm install --save-dev @playwright/test @types/node

# Install Playwright browsers
npx playwright install

Create a Playwright configuration file:

playwright.config.ts
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.

Terminal
# Install DevInbox SDK
npm install --save-dev @dev-inbox/client-api

For 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:

  • 1
    Create 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.

  • 2
    Configure 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.

  • 3
    Generate 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.

  • 4
    Interact 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.

  • 5
    Verify 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:

Terminal
mkdir tests

Create the integration test file:

tests/email-verification.spec.ts
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:

package.json
{
  "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:

Terminal
# 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 test

Troubleshooting

If you encounter Error: No tests found when running npm test, verify the following:

  • The tests directory exists in your project root
  • The test file tests/email-verification.spec.ts has been created
  • The playwright.config.ts file 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