best-tempmail-playwright
v1.0.0
Published
Playwright fixture for disposable email inboxes. Sign up, wait for the verification email, read the code, in three lines.
Maintainers
Readme
best-tempmail-playwright
Disposable email inboxes as a Playwright fixture. Sign up, wait for the verification email, read the code.
npm install -D best-tempmail-playwrightimport { test, expect } from "best-tempmail-playwright";
test("a new user can verify their email", async ({ page, tempMail }) => {
const inbox = await tempMail.createInbox();
await page.goto("/signup");
await page.fill("#email", inbox.address);
await page.click("#submit");
await page.fill("#code", await inbox.waitForCode());
await page.click("#verify");
await expect(page.getByText("Welcome")).toBeVisible();
});No mail server to run, no shared test inbox, no polling loop. The inbox is created for that test and deleted when it finishes.
Setup
Set your API key once:
export BEST_TEMPMAIL_API_KEY=btm_sk_live_...Get one at best-tempmail.com/api. Code extraction needs a paid plan; everything else works on the free tier, which allows three inboxes per IP per day and needs no key at all.
Import test from this package instead of @playwright/test and every test gains tempMail. If you already extend test with your own fixtures, compose instead:
import { test as base } from "@playwright/test";
import { tempMailFixture, type TempMailFixture } from "best-tempmail-playwright";
export const test = base.extend<{ tempMail: TempMailFixture } & MyFixtures>({
...tempMailFixture(),
...myFixtures,
});Getting the code
waitForCode() waits for mail and pulls the verification code out of it, so you don't write a regex per service:
const code = await inbox.waitForCode();It throws rather than returning null, with a message that says which inbox and how long it waited, because a null surfaces three lines later as something much harder to read:
No email arrived at [email protected] within 60s.
Check that the flow under test actually sent to this address, and that the
sending service is not itself delayed.If mail arrives but no code can be trusted, you get the subject, the sender and the codes that were considered:
A message arrived at [email protected] but no verification code scored highly
enough to be trusted.
subject: Welcome to Acme
from: "Acme" <[email protected]>
considered: 2026 (score 2)That is deliberate. A wrong code fails a test in a way that is very hard to trace, so the API returns nothing rather than guessing.
Waiting for a particular email
A signup flow often sends more than one message. Filter by sender or subject, with a string (case-insensitive substring) or a regex:
const code = await inbox.waitForCode({ from: "[email protected]" });
const msg = await inbox.waitForMessage({ subject: /reset your password/i });Each message is offered once. Two waits in a row give you two different emails, in the order they arrived:
const welcome = await inbox.waitForMessage({ subject: "Welcome" });
const receipt = await inbox.waitForMessage({ subject: "Your receipt" });Timeouts are per call, in milliseconds, defaulting to 60 seconds:
await inbox.waitForCode({ timeout: 120_000 });Reading the whole message
const msg = await inbox.waitForMessage();
const full = await inbox.read(msg.id);
full.text;
full.html;
full.attachments; // metadata on every planinbox.messages() lists what has arrived, newest first, without waiting.
Two things it handles that a plain HTTP client does not
Mail that arrives before you ask for it. The API's long-poll treats anything already in the inbox when the call starts as seen. If your app's mailer is fast, the email can land between clicking submit and calling waitForCode(), and a naive wait then sits there until it times out. This reads the inbox before every poll, so that window is closed.
The concurrent-wait limit. The API allows a small number of long-polls open at once per key, shared across every Playwright worker and every machine running the suite. Each worker holds at most one by default, and a worker that does hit the limit waits and retries within your timeout rather than failing the test. If you run many workers and see waits queuing, lower workers in your Playwright config or raise your plan rather than raising maxConcurrentWaits.
Options
tempMailFixture({
apiKey: process.env.MY_KEY, // default: BEST_TEMPMAIL_API_KEY, then BTM_API_KEY
defaultTimeout: 60_000, // ms
maxConcurrentWaits: 1, // long-polls per worker
autoCleanup: true, // delete this test's inboxes when it ends
clientOptions: { baseUrl: "..." },
});Turn off autoCleanup while debugging a failure if you want to inspect what actually arrived. Inboxes expire on their own after two hours regardless.
Errors
import {
MailTimeoutError, // nothing arrived in time
NoVerificationCodeError, // mail arrived, no trustworthy code
AuthenticationError, // key missing, wrong or revoked
PaymentRequiredError, // plan does not include this (e.g. OTP on free)
RateLimitError,
NotFoundError,
} from "best-tempmail-playwright";tempMail.client exposes the underlying best-tempmail client for anything this fixture does not wrap: webhooks, attachment downloads, inbox lookups.
Requirements
Node 18 or newer, @playwright/test 1.30 or newer.
Licence
MIT
