import { APIRequestContext, request } from "@playwright/test" /** * MailDev API helper for testing email flows. * * Configure via environment variables: * - MAILDEV_URL: MailDev web UI URL (default: https://${PROJECT_NAME}-maildev.code.testversion.online) * - MAILDEV_USER: Basic auth username (default: code) * - MAILDEV_PASS: Basic auth password */ const MAILDEV_URL = process.env.MAILDEV_URL || "http://localhost:1080" const MAILDEV_USER = process.env.MAILDEV_USER || "code" const MAILDEV_PASS = process.env.MAILDEV_PASS || "" export interface MailDevEmail { id: string subject: string html: string to: { address: string; name: string }[] from: { address: string; name: string }[] date: string time: string read: boolean headers: Record } let maildevContext: APIRequestContext | null = null async function getContext(): Promise { if (!maildevContext) { const opts: any = { baseURL: MAILDEV_URL, ignoreHTTPSErrors: true, } if (MAILDEV_PASS) { opts.httpCredentials = { username: MAILDEV_USER, password: MAILDEV_PASS, } } maildevContext = await request.newContext(opts) } return maildevContext } /** * Get all emails from MailDev. */ export async function getAllEmails(): Promise { const ctx = await getContext() const res = await ctx.get("/email") if (!res.ok()) { throw new Error(`MailDev API error: ${res.status()} ${res.statusText()}`) } return res.json() } /** * Delete all emails in MailDev. */ export async function deleteAllEmails(): Promise { const ctx = await getContext() await ctx.delete("/email/all") } /** * Delete a specific email by ID. */ export async function deleteEmail(id: string): Promise { const ctx = await getContext() await ctx.delete(`/email/${id}`) } /** * Wait for an email to arrive for a specific recipient. */ export async function waitForEmail( toEmail: string, options: { subject?: string | RegExp timeout?: number pollInterval?: number } = {} ): Promise { const { subject, timeout = 5000, pollInterval = 250 } = options const start = Date.now() while (Date.now() - start < timeout) { const emails = await getAllEmails() const match = emails.find((e) => { const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase()) if (!toMatch) return false if (!subject) return true if (typeof subject === "string") return e.subject.includes(subject) return subject.test(e.subject) }) if (match) return match await new Promise((r) => setTimeout(r, pollInterval)) } throw new Error(`Timeout: No email to ${toEmail} received within ${timeout}ms`) } /** * Extract a 6-digit verification code from an email's HTML body. */ export function extractCode(email: MailDevEmail): string { const match = email.html.match(/>(\d{6})<\//) if (!match) { throw new Error(`No 6-digit code found in email "${email.subject}"`) } return match[1] } /** * Perform an action and capture the verification code from the resulting email. * Filters out emails that existed before the action was triggered. */ export async function captureCode( toEmail: string, action: () => Promise, options: { subject?: string | RegExp; timeout?: number } = {} ): Promise { const existingEmails = await getAllEmails() const existingIds = new Set( existingEmails .filter((e) => e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase())) .map((e) => e.id) ) await action() const { timeout = 5000, subject } = options const pollInterval = 250 const start = Date.now() while (Date.now() - start < timeout) { const emails = await getAllEmails() const match = emails.find((e) => { if (existingIds.has(e.id)) return false const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase()) if (!toMatch) return false if (!subject) return true if (typeof subject === "string") return e.subject.includes(subject) return subject.test(e.subject) }) if (match) return extractCode(match) await new Promise((r) => setTimeout(r, pollInterval)) } throw new Error(`Timeout: No new email to ${toEmail} received within ${timeout}ms`) } /** * Dispose the MailDev API context. Call in globalTeardown. */ export async function disposeMailDev(): Promise { if (maildevContext) { await maildevContext.dispose() maildevContext = null } }