email-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<objective>
Email-dependent flows fail silently: a test that "signs up and clicks confirm" by calling
the confirm endpoint directly never proves the email was generated, addressed, templated,
and linkable. This skill captures the real email, waits for it without a fixed sleep,
extracts the OTP or link from the body with an anchored regex, and completes the flow —
so a broken template, an unsigned token, or a wrong-recipient bug actually fails the test.
It also keeps deliverability (SPF/DKIM/DMARC) in a separate non-blocking suite so a DNS
problem never reds your functional gate.
</objective>
<objective>
依赖邮件的流程会静默失败:直接调用确认端点来「完成注册并点击确认」的测试,永远无法证明邮件已生成、正确寻址、完成模板渲染且链接可用。本技能会捕获真实邮件,无需固定等待即可等待邮件送达,使用锚定正则从正文中提取OTP或链接,并完成整个流程——这样一来,模板损坏、令牌签名错误或收件人错误等问题都会导致测试失败。此外,它还将可送达性(SPF/DKIM/DMARC)检查放在独立的非阻塞套件中,确保DNS问题不会导致功能测试关卡变红。
</objective>
Quick Route
快速指引
| Situation | Go to |
|---|---|
| Pick a capture tool | "Capture-inbox decision tree" below |
| Poll an inbox without a sleep | |
| Pull an OTP / link out of the body | |
| Full password-reset / signup / magic-link E2E | |
| Real addresses on staging | |
| Per-test throwaway inbox | |
| Just preview a template locally | |
| Flag SPF/DKIM/DMARC problems | |
| Tests pass locally, "no email yet" in CI | "Flaky email in CI" below |
| 场景 | 查看内容 |
|---|---|
| 选择捕获工具 | 下方的「捕获收件箱决策树」 |
| 无需等待即可轮询收件箱 | |
| 从正文中提取OTP/链接 | |
| 完整的密码重置/注册/魔法链接端到端测试 | |
| 预发布环境使用真实地址 | |
| 每个测试使用一次性收件箱 | |
| 仅在本地预览模板 | |
| 标记SPF/DKIM/DMARC问题 | |
| 本地测试通过,但CI环境提示「邮件未送达」 | 下方的「CI环境中邮件不稳定」 |
Discovery Questions
调研问题
Check first — if it exists, use it and skip anything
answered there.
.agents/qa-project-context.md- Where does the email need to be received? Local capture (Mailpit) covers most functional flows for free. A real, externally-deliverable address (staging, a third- party ESP, real DNS) means a hosted inbox (Mailosaur / MailSlurp). This is the single biggest tool-selection driver.
- How parallel is the suite? High parallelism makes "the latest email" ambiguous — you need per-test unique addresses or per-test inboxes, not a shared mailbox.
- Which flows? Signup confirmation, password reset, magic-link login, OTP/MFA, notification emails — they share one shape (capture, extract, complete) but differ in what you extract (link vs 6-digit code).
- Is deliverability in scope? "Lands in inbox / passes SPF, DKIM, DMARC" is a separate non-blocking suite, not part of the functional OTP test. Decide upfront.
- Local-only preview, or CI assertions? Previewing a template during dev is Ethereal; asserting in CI is Mailpit/Mailosaur/MailSlurp. Don't confuse the two.
首先查看——如果存在,使用其中信息并跳过已解答的问题。
.agents/qa-project-context.md- 邮件需要接收至何处? 本地捕获工具(Mailpit)可免费覆盖大多数功能流程。需要真实、可外部送达的地址(预发布环境、第三方ESP、真实DNS)时,需使用托管收件箱(Mailosaur/MailSlurp)。这是工具选择的核心决定因素。
- 测试套件的并行度如何? 高并行度会导致「最新邮件」的定义模糊——你需要每个测试专属的唯一地址或收件箱,而非共享邮箱。
- 需要测试哪些流程?** 注册确认、密码重置、魔法链接登录、OTP/MFA、通知邮件——它们的流程形态一致(捕获、提取、完成),但提取内容不同(链接 vs 6位验证码)。
- 可送达性是否在测试范围内? 「邮件送达收件箱/通过SPF、DKIM、DMARC校验」属于独立的非阻塞套件,不属于功能OTP测试的一部分。需提前确定。
- 仅本地预览,还是CI环境断言? 开发期间预览模板使用Ethereal;CI环境中断言使用Mailpit/Mailosaur/MailSlurp。请勿混淆两者。
Core Principles
核心原则
-
Capture the real email; never shortcut past it. Calling the reset endpoint directly or hardcoding a token skips the exact integration the test exists to cover — templating, recipient resolution, link generation, token signing. Submit on the UI, read the inbox.
-
Poll, never sleep. Email arrival is asynchronous. A fixedis either too short (flake) or too slow (wasted minutes) and is the #1 cause of flaky email tests. Use
waitForTimeout/expect.pollagainst.toPass, or a built-in waiter (/api/v1/messages,messages.get) that already polls for you.waitForLatestEmail -
One unique recipient per test. Two parallel tests both reading "the latest signup email" grab each other's mail. Make every test's address unique (plus-addressing or a per-test inbox) and filter by recipient when reading. Clearing the inbox between tests does NOT survive parallelism.
-
Extract from the body with an anchored regex, guarded. Pull the OTP / link from the email body (/
message.Text), not the live page. Usemessage.HTML/\d{6}and assert the match is not null — a missing code must fail loudly, not become.match(.undefined -
Assert content, not existence. "An email arrived" is a weak assertion. Check the, the
subjectaddress, and that links point to the right domain. Wrong-template and wrong-link bugs only surface if you assert on content.from -
Deliverability is a separate, non-blocking suite. SPF/DKIM/DMARC come from a real receiving server's authentication, never from the body text — and they must not block the functional flow tests.
-
捕获真实邮件,绝不走捷径。 直接调用重置端点或硬编码令牌会跳过测试本应覆盖的集成环节——模板渲染、收件人解析、链接生成、令牌签名。通过UI提交请求,读取收件箱内容。
-
轮询,绝不固定等待。 邮件送达是异步的。固定的要么时间过短(导致测试不稳定),要么时间过长(浪费时间),是邮件测试不稳定的头号原因。使用
waitForTimeout/expect.poll调用.toPass,或使用内置等待器(/api/v1/messages、messages.get),这些工具已内置轮询逻辑。waitForLatestEmail -
每个测试对应唯一收件人。 两个并行测试都读取「最新注册邮件」时,会互相获取对方的邮件。为每个测试生成唯一地址(加号寻址或专属收件箱),并在读取时按收件人过滤。测试间清空收件箱无法解决并行问题。
-
使用带防护的锚定正则从正文中提取内容。 从邮件正文(/
message.Text)提取OTP/链接,而非从实时页面提取。使用message.HTML/\d{6}并断言匹配结果不为空——缺失的验证码必须直接导致测试失败,而非变为.match()。undefined -
断言内容,而非仅存在性。 「邮件已送达」是弱断言。需检查、
subject地址,以及链接指向的域名是否正确。只有断言内容,才能发现模板错误和链接错误的问题。from -
可送达性检查是独立的非阻塞套件。 SPF/DKIM/DMARC校验结果来自真实接收服务器的认证,而非正文文本——且这些检查不得阻塞功能流程测试。
Capture-inbox decision tree
捕获收件箱决策树
Pick the cheapest tool that can actually receive your mail. Default to Mailpit for
local + CI functional tests; reach for a hosted inbox only when you need real addresses or
high-parallelism isolation.
| Tool | Hosting / cost | Address type | Use when |
|---|---|---|---|
| Mailpit | Self-host, single binary or docker, free / open source | Any local SMTP recipient | Default. Local + GitHub Actions functional tests, tight budget. REST API at |
| Mailosaur | Hosted (API key), paid | Real | Staging/prod-like flows needing a real deliverable address; auto-waiting |
| MailSlurp | Hosted (API key), paid | Real, per-inbox | Per-test throwaway inboxes via |
| Ethereal | Hosted throwaway, free | Captures, delivers nothing | Local-dev template preview only ( |
For the budget signup-flow case (local + GitHub Actions, self-host fine): use Mailpit
— a single binary / docker image with a free, open-source REST API at . If you
later need a real deliverable address, graduate that suite to Mailosaur or
MailSlurp (hosted inboxes with real addresses). Do not "just check the database instead
of the email" — that proves the row was written, not that the email was sent, addressed,
and linkable.
:8025Avoid: MailHog — archived/unmaintained since 2020; Mailpit is its drop-in replacement
(same ports, compatible API), verified mid-2026. Also skip smtp4dev / Papercut for new
suites — Mailpit's API and full-text search are better for automated assertions.
See for the docker-compose, the polling helper, and the
extraction utilities; for Mailosaur / MailSlurp / Ethereal.
references/mailpit-playwright.mdreferences/hosted-inboxes.md选择能够接收邮件的最低成本工具。默认使用Mailpit进行本地+CI功能测试;仅当需要真实地址或高并行隔离时,才使用托管收件箱。
| 工具 | 托管方式/成本 | 地址类型 | 适用场景 |
|---|---|---|---|
| Mailpit | 自托管,单二进制文件或Docker镜像,免费/开源 | 任意本地SMTP收件人 | 默认选项。本地+GitHub Actions功能测试、预算有限场景。REST API地址为 |
| Mailosaur | 托管(需API密钥),付费 | 真实 | 需要真实可送达地址的预发布/生产类流程;内置自动等待的 |
| MailSlurp | 托管(需API密钥),付费 | 真实专属收件箱 | 通过 |
| Ethereal | 托管一次性收件箱,免费 | 仅捕获,不实际送达 | 仅用于本地开发时的模板预览( |
对于预算有限的注册流程场景(本地+GitHub Actions,自托管即可):使用Mailpit——单二进制文件/Docker镜像,免费开源,REST API地址为。如果后续需要真实可送达地址,可将套件升级为Mailosaur或MailSlurp(带真实地址的托管收件箱)。请勿「仅检查数据库而非邮件」——这只能证明数据库行已写入,无法证明邮件已发送、正确寻址且链接可用。
:8025避免使用:MailHog——自2020年起已归档/停止维护;Mailpit是其无缝替代工具(相同端口、兼容API),已在2026年年中验证。新套件也请跳过smtp4dev/Papercut——Mailpit的API和全文搜索更适合自动化断言。
查看获取Docker Compose配置、轮询辅助工具和提取工具;查看获取Mailosaur/MailSlurp/Ethereal的相关内容。
references/mailpit-playwright.mdreferences/hosted-inboxes.mdPolling an inbox (Mailpit)
轮询收件箱(Mailpit)
Read the list endpoint with Playwright's fixture, find the message addressed to
this test's recipient, then fetch the full body by ID. Use with a
and — it retries until a match appears, so fast inboxes resolve instantly and
slow ones still pass.
requestexpect.polltimeoutintervalsts
// `request` is the Playwright APIRequestContext fixture; plain fetch() works too.
await expect.poll(async () => {
const res = await request.get(`http://localhost:8025/api/v1/messages?query=to:${encodeURIComponent(to)}`);
const { messages } = await res.json();
return messages.find((m) => m.To.some((t) => t.Address === to))?.ID ?? null;
}, { timeout: 30_000, intervals: [500, 1_000, 2_000] }).not.toBeNull();
// then: request.get(`http://localhost:8025/api/v1/message/${id}`) → { Text, HTML, Subject, From, To }The filter plus the on the recipient is what makes parallel tests
deterministic. Never take / (newest overall) with no
recipient filter. Full helper in .
query=to:.findmessages[0]messages.at(-1)references/mailpit-playwright.md使用Playwright的 fixture读取列表端点,找到发送至当前测试收件人的邮件,然后通过ID获取完整正文。使用带和的——它会重试直到找到匹配项,因此快速收件箱可立即返回结果,慢收件箱也能通过测试。
requesttimeoutintervalsexpect.pollts
// `request`是Playwright的APIRequestContext fixture;使用普通fetch()也可。
await expect.poll(async () => {
const res = await request.get(`http://localhost:8025/api/v1/messages?query=to:${encodeURIComponent(to)}`);
const { messages } = await res.json();
return messages.find((m) => m.To.some((t) => t.Address === to))?.ID ?? null;
}, { timeout: 30_000, intervals: [500, 1_000, 2_000] }).not.toBeNull();
// 后续:request.get(`http://localhost:8025/api/v1/message/${id}`) → { Text, HTML, Subject, From, To }query=to:.findmessages[0]messages.at(-1)references/mailpit-playwright.mdExtracting OTPs and links
提取OTP和链接
Match against the email body, anchored, with a null guard:
ts
const otp = body.match(/\b(\d{6})\b/)?.[1];
expect(otp, 'no OTP in email body').toBeTruthy(); // throw / fail if null
const link = body.match(/https?:\/\/\S*(?:verify|confirm|reset|token=)\S*/i)?.[0];
if (!link) throw new Error('no verification link in email body');Do NOT slice by index (, , ) — those
break the moment the template changes a word. Do NOT read of the live page when
you mean the email body. If a 6-digit code could collide with other numbers, anchor on the
label: . Hosted services expose structured
/ — prefer those when available. See
.
body.split(' ')[3]substring(0, 6)indexOf('code')innerTextbody.match(/code[:\s]+(\d{6})/i)message.html.linksmessage.html.codesreferences/mailpit-playwright.md针对邮件正文进行锚定匹配,并添加空值防护:
ts
const otp = body.match(/\b(\d{6})\b/)?.[1];
expect(otp, '邮件正文中无OTP').toBeTruthy(); // 为空时抛出错误/测试失败
const link = body.match(/https?:\/\/\S*(?:verify|confirm|reset|token=)\S*/i)?.[0];
if (!link) throw new Error('邮件正文中无验证链接');请勿使用索引切片(、、)——模板稍有改动就会失效。请勿误将实时页面的当作邮件正文读取。如果6位验证码可能与其他数字冲突,可锚定标签:。托管服务会暴露结构化的/——如有可用请优先使用。详见。
body.split(' ')[3]substring(0, 6)indexOf('code')innerTextbody.match(/code[:\s]+(\d{6})/i)message.html.linksmessage.html.codesreferences/mailpit-playwright.mdDeterministic addresses (parallel isolation)
确定性地址(并行隔离)
The parallel-flake bug: two tests sign up at once and both poll for "the latest signup
email," so they swap messages. Fixes, in order of preference:
- Per-test unique address — plus-addressing / sub-addressing:
, or
user+${randomUUID()}@example.com. Most providers routesignup.${Date.now()}@...touser+anything@, so one real mailbox yields infinite unique recipients.user@ - Filter every read by recipient — (Mailosaur) or a
sentTo+query=to:match (Mailpit). Never take the newest message overall..find - Per-test dedicated inbox — MailSlurp gives each test its own inbox; Mailosaur gives each test a unique address on your server domain.
createInbox()
Clearing the inbox between tests is not sufficient under parallelism — two tests
running at the same instant still collide. Unique address + recipient filter is the real
fix. See .
references/hosted-inboxes.md并行测试不稳定的问题:两个测试同时注册,都轮询「最新注册邮件」,导致邮件被互相获取。修复方案按优先级排序:
- 每个测试使用唯一地址——加号寻址/子寻址:,或
user+${randomUUID()}@example.com。大多数邮件提供商都会将signup.${Date.now()}@...路由至user+anything@,因此一个真实邮箱可生成无限个唯一收件人。user@ - 每次读取都按收件人过滤——使用Mailosaur的或Mailpit的
sentTo+query=to:匹配。绝不要直接取全局最新邮件。.find - 每个测试使用专属收件箱——MailSlurp的可为每个测试生成专属收件箱;Mailosaur可为每个测试在你的服务器域名下生成唯一地址。
createInbox()
测试间清空收件箱不足以解决并行问题——同时运行的两个测试仍会冲突。唯一地址+收件人过滤才是根本解决方法。详见。
references/hosted-inboxes.mdAsserting subject / from / headers / links
断言主题/发件人/头部/链接
After capture, assert on content:
- — catches wrong-template bugs.
expect(message.subject).toBe('Welcome to Example') - — catches misconfigured sender / reply-to.
expect(message.from?.[0].email).toBe('hello@example.com') - Headers (, custom
List-Unsubscribeheaders) when your product sets them.X- - Links point to the right domain:
.
expect(links.every((l) => new URL(l.href).hostname.endsWith('staging.example.com'))).toBe(true)
Mailosaur example asserting , , and link domain is in
.
subjectfromreferences/hosted-inboxes.md捕获邮件后,对内容进行断言:
- ——捕获模板错误问题。
expect(message.subject).toBe('Welcome to Example') - ——捕获发件人/回复地址配置错误问题。
expect(message.from?.[0].email).toBe('hello@example.com') - 当产品设置了特定头部(、自定义
List-Unsubscribe头部)时,也需断言这些头部。X- - 链接指向正确域名:。
expect(links.every((l) => new URL(l.href).hostname.endsWith('staging.example.com'))).toBe(true)
Mailosaur断言、和链接域名的示例见。
subjectfromreferences/hosted-inboxes.mdDeliverability: SPF / DKIM / DMARC
可送达性:SPF/DKIM/DMARC
Keep this in its own non-blocking suite, separate from functional flow tests. SPF,
DKIM, and DMARC / + alignment come from a real receiving server authenticating
your sending domain — they are not strings in the body, so never
or . And Mailpit does not validate
SPF/DKIM/DMARC alignment — it only does basic SpamAssassin content scoring, because
nothing was sent over real DNS. Real alignment needs a hosted send-and-receive
(Mailosaur deliverability report, or mail-tester.com for a one-off). Tag the suite
, run it as a non-required CI job (), and never assert
deliverability inside the OTP / reset flow test. See .
passfailbody.includes('spf')body.match(/dkim/)@deliverabilitycontinue-on-errorreferences/deliverability.md将此检查放在独立的非阻塞套件中,与功能流程测试分离。SPF、DKIM和DMARC的/+对齐结果来自真实接收服务器对发送域名的认证——它们不是正文中的字符串,因此绝不要使用或。此外,Mailpit不验证SPF/DKIM/DMARC对齐——它仅进行基础的SpamAssassin内容评分,因为邮件并未通过真实DNS发送。真实对齐检查需要托管的发送-接收服务(Mailosaur的可送达性报告,或一次性使用mail-tester.com)。为套件标记,将其作为CI的非必填任务(),绝不要在OTP/重置流程测试中断言可送达性。详见。
passfailbody.includes('spf')body.match(/dkim/)@deliverabilitycontinue-on-errorreferences/deliverability.mdFlaky email in CI
CI环境中邮件不稳定
Tests pass locally but the email "hasn't arrived yet" when CI asserts. The four root
causes — diagnose all of them, do not just bump the sleep:
- A fixed sleep instead of polling. / arbitrary delay races the email. Replace with
waitForTimeout/expect.poll/ a built-in.toPass.waitFor - Timeout too short. CI mail delivery is slower than local. Once polling, increase the
poll (e.g. 30–60s) rather than adding a longer blind sleep.
timeout - No recipient filter. Reading the newest message overall picks up another test's mail
under parallelism. Filter by /
sentToand use a unique address per test.to: - Stale messages from a previous run. An old matching email satisfies the assertion before the new one arrives. Clear the inbox in global setup and/or make the address unique per run so prior-run mail can't match.
Retrying the whole job, quarantining the test, or raising a global sleep to 30s treats the
symptom and leaves the race in place.
本地测试通过,但CI环境中断言时提示「邮件未送达」。四个根本原因——需全部排查,不要仅增加等待时间:
- 使用固定等待而非轮询。 /任意延迟会与邮件送达竞速。替换为
waitForTimeout/expect.poll/内置.toPass。waitFor - 超时时间过短。 CI环境中的邮件送达比本地慢。使用轮询后,增加轮询(如30–60秒),而非增加更长的盲目等待。
timeout - 无收件人过滤。 读取全局最新邮件会在并行测试时获取其他测试的邮件。按/
sentTo过滤,并为每个测试使用唯一地址。to: - 前一次运行的陈旧邮件。 旧的匹配邮件会在新邮件到达前满足断言。在全局设置中清空收件箱,或为每次运行生成唯一地址,确保前一次运行的邮件无法匹配。
重试整个任务、隔离测试或全局等待时间增加至30秒只是治标不治本,仍会存在竞速问题。
Anti-Patterns
反模式
1. Fixed sleep before reading the inbox
1. 读取收件箱前使用固定等待
page.waitForTimeout(5000)setTimeoutsleep()expect.poll/api/v1/messagespage.waitForTimeout(5000)setTimeoutsleep()expect.poll/api/v1/messages2. Recommending a dead capture tool
2. 推荐已废弃的捕获工具
MailHog is archived (2020). Papercut / smtp4dev are weaker for automation. Use Mailpit.
MailHog已归档(2020年)。Papercut/smtp4dev的自动化能力较弱。请使用Mailpit。
3. Checking the database instead of the email
3. 检查数据库而非邮件
A DB row proves the write happened, not that the email was sent, addressed, and linkable.
Read the actual captured message.
数据库行仅能证明写入操作已完成,无法证明邮件已发送、正确寻址且链接可用。请读取实际捕获的邮件。
4. Shortcutting past the email
4. 跳过邮件环节
Calling the reset endpoint directly or hardcoding a token skips templating, link
generation, and token signing. Capture the email, extract the link, it.
page.goto直接调用重置端点或硬编码令牌会跳过模板渲染、链接生成和令牌签名环节。请捕获邮件、提取链接、调用访问链接。
page.goto5. Brittle index-based extraction
5. 使用脆弱的索引提取方式
body.split(' ')[3]substring(0, 6)indexOf('code')\d{6}innerTextbody.split(' ')[3]substring(0, 6)indexOf('code')\d{6}innerText6. Newest-message-overall with no recipient filter
6. 无收件人过滤直接取全局最新邮件
messages[0]messages.at(-1)messages[0]messages.at(-1)7. Asserting only that an email exists
7. 仅断言邮件存在
No / / link checks misses wrong-template and wrong-link bugs. Assert
content.
subjectfrom不检查//链接会遗漏模板错误和链接错误问题。请断言内容。
subjectfrom8. Regexing SPF/DKIM/DMARC out of the body, or trusting Mailpit for it
8. 从正文中正则匹配SPF/DKIM/DMARC,或信任Mailpit的结果
Auth results come from a real receiving server, not body text; Mailpit only does spam
scoring. Use Mailosaur's deliverability report, in a separate non-blocking suite.
认证结果来自真实接收服务器,而非正文文本;Mailpit仅进行垃圾邮件评分。请使用Mailosaur的可送达性报告,并放在独立的非阻塞套件中。
9. IMAP libraries against a real mailbox
9. 在真实邮箱中使用IMAP库
imap-simplenode-imapimapflowcreateInboxwaitForLatestEmailimap-simplenode-imapimapflowcreateInboxwaitForLatestEmail10. Ethereal in CI, or a paid service for a local preview
10. 在CI环境中使用Ethereal,或为本地预览使用付费服务
Ethereal delivers nothing — it's preview-only ( + ).
Don't assert on it in CI; equally, don't spin up a paid hosted service just to eyeball a
template locally.
createTestAccountgetTestMessageUrlEthereal不实际送达邮件——仅用于预览(+)。请勿在CI环境中断言;同样,不要为了本地查看模板就启用付费托管服务。
createTestAccountgetTestMessageUrlVerification
验证
Prove the suite actually captures and asserts, smallest check first:
- Capture is wired: returns a number (Mailpit up, API reachable). For a hosted inbox, a one-line
curl -s localhost:8025/api/v1/messages | jq '.total'/messages.getsmoke script returns without auth error.createInbox - No blind sleeps: over the email specs prints nothing.
grep -rE 'waitForTimeout|sleep\(|setTimeout' tests/ - The flow is green and real: run the signup/reset spec and confirm it fails when you temporarily break the template subject — if it still passes, you are not asserting content.
- Determinism holds: run the email specs with ; a passing run proves the per-recipient filter survives parallelism.
--workers=4 --repeat-each=3 - Deliverability is isolated: selects only the auth suite, and that CI job is
--grep @deliverability/ non-required.continue-on-error
证明套件确实能捕获并断言内容,从最小检查项开始:
- 捕获已配置完成: 返回数字(Mailpit已启动,API可访问)。对于托管收件箱,单行
curl -s localhost:8025/api/v1/messages | jq '.total'/messages.get冒烟脚本可正常返回且无认证错误。createInbox - 无盲目等待: 在邮件测试文件中执行无输出。
grep -rE 'waitForTimeout|sleep\(|setTimeout' tests/ - 流程正常且真实: 运行注册/重置测试,并确认临时修改模板主题时测试会失败——如果仍通过,说明未断言内容。
- 确定性达标: 使用运行邮件测试;测试通过证明按收件人过滤可应对并行场景。
--workers=4 --repeat-each=3 - 可送达性检查已隔离: 仅选中认证套件,且该CI任务为
--grep @deliverability/非必填。continue-on-error
Done When
完成标准
- The capture tool is chosen against the decision tree and recorded in
(Mailpit for local/CI, a hosted inbox only where a real address is needed).
.agents/qa-project-context.md - No email test contains /
waitForTimeout/sleepbefore reading the inbox —setTimeoutover the email specs is clean; arrival is awaited viagrep -rE 'waitForTimeout|sleep\(|setTimeout' tests//expect.poll/ a built-in waiter..toPass - Each email test uses a unique recipient (plus-address or per-test inbox) and filters
reads by that recipient — no /
messages[0]without a filter.messages.at(-1) - OTP/link extraction uses an anchored regex (,
\d{6}) with a null guard that fails the test on no match.https?://... - At least the signup-confirmation (or reset / magic-link) flow has a green E2E test that
captures the real email and completes the flow through .
page.goto(link) - Content assertions on ,
subject, and link domain exist — not just existence.from - Deliverability (SPF/DKIM/DMARC) tests, if in scope, live in a separate suite that is non-blocking in CI.
@deliverability
- 根据决策树选择捕获工具,并记录在中(本地/CI使用Mailpit,仅在需要真实地址时使用托管收件箱)。
.agents/qa-project-context.md - 所有邮件测试在读取收件箱前均未使用/
waitForTimeout/sleep——在邮件测试文件中执行setTimeout无输出;通过grep -rE 'waitForTimeout|sleep\(|setTimeout' tests//expect.poll/内置等待器等待邮件送达。.toPass - 每个邮件测试使用唯一收件人(加号寻址或专属收件箱),并按该收件人过滤读取内容——无过滤时绝不使用/
messages[0]。messages.at(-1) - OTP/链接提取使用锚定正则(、
\d{6}),并添加空值防护,无匹配时测试失败。https?://... - 至少注册确认(或重置/魔法链接)流程有一个通过的端到端测试,可捕获真实邮件并通过完成流程。
page.goto(link) - 存在对、
subject和链接域名的内容断言——而非仅断言存在性。from - 如果可送达性(SPF/DKIM/DMARC)测试在范围内,它们位于独立的套件中,且在CI环境中为非阻塞。
@deliverability
Related Skills
相关技能
- playwright-automation — the browser-driving half of every email flow: forms, navigation, fixtures, and the poll helpers. This skill adds the inbox side.
- api-testing — go there to test the email provider's API directly or to test that your app sends mail; this skill is about receiving and asserting in an E2E flow.
- test-data-management — generating unique per-test addresses, factories, and seeded users that feed the recipient strategy here.
- qa-project-context — records the chosen capture tool, SMTP target, and credentials so every email test shares one configuration.
- playwright-automation——每个邮件流程的浏览器驱动部分:表单、导航、fixture和轮询辅助工具。本技能补充了收件箱相关的内容。
- api-testing——如需直接测试邮件提供商的API,或测试应用是否发送邮件,请参考该技能;本技能聚焦于端到端流程中的接收和断言。
- test-data-management——生成每个测试专属的唯一地址、工厂类和种子用户,为收件人策略提供支持。
- qa-project-context——记录所选捕获工具、SMTP目标和凭证,确保所有邮件测试共享同一配置。
Reference Files (in references/
)
references/参考文件(位于references/
)
references/- mailpit-playwright.md — docker-compose, the Mailpit helper, OTP/link extraction utilities, and full password-reset / signup / OTP / magic-link E2E tests.
expect.poll - hosted-inboxes.md — Mailosaur (auto-wait, real addresses, structured links/codes), MailSlurp (
messages.get+createInbox, per-test inbox), and Ethereal (local preview viawaitForLatestEmail+createTestAccount).getTestMessageUrl - deliverability.md — SPF/DKIM/DMARC as a separate non-blocking suite, why body-regex and Mailpit don't validate auth, and the Mailosaur deliverability assertion.
- mailpit-playwright.md——Docker Compose配置、Mailpit辅助工具、OTP/链接提取工具,以及完整的密码重置/注册/OTP/魔法链接端到端测试。
expect.poll - hosted-inboxes.md——Mailosaur(自动等待、真实地址、结构化链接/验证码)、MailSlurp(
messages.get+createInbox、专属测试收件箱)和Ethereal(通过waitForLatestEmail+createTestAccount进行本地预览)的相关内容。getTestMessageUrl - deliverability.md——SPF/DKIM/DMARC作为独立非阻塞套件的实现方式、为什么正文正则和Mailpit无法验证认证,以及Mailosaur可送达性断言的方法。