Playwright
Create an inbox, let the app send normally, wait, then follow the link.
Playwright consumes @mailwurf/e2e. It is not a dependency of the package.
The app under test keeps its production sender. The test only sets the recipient to inbox.address.
import { expect, test } from "@playwright/test";
import { createMailwurf } from "@mailwurf/e2e";
const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!); // API key (mw_live_…) from Dashboard → API keys
test("confirms signup from the inbox", async ({ page }) => {
const inbox = await mailwurf.createInbox();
await page.goto(`${process.env.APP_URL ?? "http://localhost:3000"}/signup`);
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Sign up" }).click();
const mail = await inbox.waitFor({ subject: "Confirm", timeout: 15_000 });
const link = mail.links.find((url) => url.includes("/confirm"));
if (!link) {
throw new Error(`confirm mail had no confirm link: ${mail.links.join(", ")}`);
}
await page.goto(link.replaceAll("&", "&"));
await expect(page.getByText("confirmed")).toBeVisible();
});A fixture per test
A fixture gives every test its own inbox and removes it afterwards.
import { test as base } from "@playwright/test";
import { createMailwurf, type MailwurfInbox } from "@mailwurf/e2e";
const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!);
export const test = base.extend<{ inbox: MailwurfInbox }>({
inbox: async ({}, use) => {
const inbox = await mailwurf.createInbox();
await use(inbox);
await inbox.delete().catch(() => {});
},
});
export { expect } from "@playwright/test";import { expect, test } from "./fixtures";
test("signs in with a one-time code", async ({ page, inbox }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Send code" }).click();
const mail = await inbox.waitFor({ subject: "code" });
await page.getByLabel("Code").fill(mail.otp);
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});Workers
Each API key can wait on four inboxes at once. With workers above four, give each CI job its own key or keep workers at four or fewer. See Waiting.
CI needs MAILWURF_API_KEY and the app origin. Pass apiUrl to createMailwurf() only for a self-hosted Mailwurf. See CI.
Mailwurf