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_KEY as a CI secret. Never commit it.
  • Use received_after if a previous matching email could still be in the inbox.
  • Delete the inbox in a finally path 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/signup

New to the API? Read the InboxRhino API quickstart. Quotas are listed on InboxRhino pricing.