Playwright email testing
Playwright email testing without a plugin: use fetch or Playwright's request API against a real InboxRhino inbox. Cover signup verification, OTP codes, magic links and password resets without MailHog or a USD QA platform. Wait up to 180 seconds, then delete the inbox so quota is released.
Signup verification example
The pattern is the same for OTP and magic-link tests: allocate an address, drive the UI, wait with a subject or sender filter, parse HTML or text, then delete the inbox. Handle HTTP 200 (match) and 204 (timeout) separately — do not parse JSON on 204. Query rules are on the messages API.
import { test, expect } from '@playwright/test';
const API = 'https://api.inboxrhino.in';
async function responseError(response: { status(): number; text(): Promise<string> }) {
return `HTTP ${response.status()}: ${await response.text()}`;
}
test('completes signup through the verification email', async ({ page, request }) => {
const key = process.env.INBOXRHINO_API_KEY;
test.skip(!key, 'Set INBOXRHINO_API_KEY');
if (!key) return;
const auth = { Authorization: `Bearer ${key}` };
const startedAt = new Date().toISOString();
const prefix = `signup-${Date.now().toString(36)}`;
let inboxId: string | undefined;
try {
const created = await request.post(`${API}/v1/inboxes`, {
headers: { ...auth, 'Content-Type': 'application/json' },
data: { prefix },
});
expect(created.status(), await responseError(created)).toBe(201);
const inbox = await created.json();
inboxId = inbox.data.id;
await page.goto(process.env.SIGNUP_URL ?? 'http://localhost:3000/signup');
await page.getByLabel('Email').fill(inbox.data.address);
await page.getByRole('button', { name: /sign up|create account/i }).click();
const query = new URLSearchParams({
wait_seconds: '180',
limit: '1',
include: 'content',
subject: 'Verify',
sender: 'noreply@example.com',
received_after: startedAt,
});
const mail = await request.get(`${API}/v1/inboxes/${inboxId}/messages?${query}`, {
headers: auth,
timeout: 190_000,
});
expect(mail.status(), mail.status() === 204
? 'No matching message arrived within 180 seconds'
: await responseError(mail)).toBe(200);
const body = await mail.json();
expect(body.data).toHaveLength(1);
expect(body.data[0].subject).toMatch(/verify/i);
const html = body.data[0].html ?? '';
const verificationUrl = html.match(/href=["'](https?:\/\/[^"']+)["']/i)?.[1];
expect(verificationUrl, 'Verification link missing from message HTML').toBeTruthy();
await page.goto(verificationUrl!);
await expect(page.getByText(/verified|account is ready/i)).toBeVisible();
} finally {
if (inboxId) {
const deleted = await request.delete(`${API}/v1/inboxes/${inboxId}`, { headers: auth });
expect(deleted.status(), await responseError(deleted)).toBe(204);
}
}
});Adapt the two application-specific lines
Replace the signup URL, form labels, sender address and final success assertion with values from your application. Keep the InboxRhino lifecycle, the received_after boundary and cleanup unchanged. If your email contains several links, parse the one your application owns rather than selecting the first link.
CI notes
- Store
INBOXRHINO_API_KEYas a CI secret. Never commit it. - Use
received_afterif a previous matching email could still be in the inbox. - Delete the inbox in a
finallypath so a failed assertion does not leak active-inbox quota. Deleting a message does not restore monthly email quota. - Local SMTP catchers such as Mailpit work on a laptop and disappear in GitHub Actions. InboxRhino is a real MX address, so the same test can run in CI.
GitHub Actions
Add INBOXRHINO_API_KEY as a repository or environment secret. Point SIGNUP_URL at a test environment that can send real email.
name: Playwright email test
on: [push]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
INBOXRHINO_API_KEY: ${{ secrets.INBOXRHINO_API_KEY }}
SIGNUP_URL: https://staging.example.com/signupNew to the API? Read the InboxRhino API quickstart. Quotas are listed on InboxRhino pricing.