跳转到内容

proxymail-qa 为 JavaScript 和 TypeScript 封装了 REST API:可用于 Playwright、Cypress 任务、Vitest、Jest、Detox 或普通脚本。它没有依赖,自带类型,可在 Node 18 或更高版本上运行(或任何支持 fetch 的环境)。源代码位于 github.com/aaquib90/proxymail-qa-js,每个版本都从 GitHub Actions 发布,并附带 npm 来源证明。

  1. npm install --save-dev proxymail-qa
  2. 创建一个 API 密钥 并设为 PROXYMAIL_QA_API_KEY,或直接传入。

  3. 创建一个地址,在你的应用中用它注册,然后等待验证码或链接。

    等待只返回验证码或链接本身,以及发送它的服务和过期时间,从不返回邮件正文。一次等待最长持续 30 秒,更长的 timeout 会重复等待。

    import { ProxyMailQA } from 'proxymail-qa';
    const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEY
    const { address, persona, password } = await qa.createAddress({ persona: true, password: true });
    const submitted = new Date();
    // …sign up in your app with address, persona and password…
    const { value: code } = await qa.waitForCode(address, { since: submitted });
    // or: const { value: link } = await qa.waitForLink(address, { type: 'verify', since: submitted });
    await qa.deleteAddress(address);

完整示例:Playwright 和 Cypress。

方法 作用
new ProxyMailQA({ apiKey?, baseUrl?, fetch?, run? }) 客户端。apiKey 默认为 PROXYMAIL_QA_API_KEY,run 默认为 PROXYMAIL_QA_RUN
createAddress(options?) 新的测试地址。选项:label、name(@ 前的自定义名称;单独使用 test 等通用名称会被拒绝)、persona、password(只返回一次,传入 password: true 时类型为 string)、lifetimeDays、domain、age 或 dateOfBirth、country,以及使用场景:site 和 environment(0.9.0 新增)
waitForCode(address, options?) 最新的验证码或登录码。选项:since、service、timeout(秒,默认 30)、unused(跳过已用过的验证码:由之前的等待返回,或在应用或 Proxy Mail QA for Chrome 中复制或填写过;0.5.1 新增),以及用于优惠券或促销码的 type: 'coupon',附带其 discount、valid_until 和 after_seconds(0.10.0 新增)
waitForLink(address, options?) 链接的用法相同。另有 type(verify、magic_signin、invite、reset 或 any,或一个列表)和 app(true 表示打开移动应用的链接)
messages(address, options?) 邮件及其验证码和链接。includeText 和 includeHtml 会附加内容,用于模板测试;limit
countEmails(address, options?) 收到了多少封邮件,每封只有摘要,不含内容。选项:since、service、type(code 或 link)、subtype,以及从 0.10.0 起的 category(auth、transactional 或 marketing)
expectNoEmail(address, options?) 如果在 timeout 秒内(默认 30,也可以设为几分钟:它会再次查询)没有收到匹配的邮件,则成功返回;一旦收到,立即抛出 ProxyMailQAError(409,email_arrived)。筛选条件相同,since 默认为当前时间
markUnsubscribed(address, options?) Labs。 在测试于浏览器中完成退订后,记录该地址已退订某个发件人(emailId 或 service;at 用于回溯日期;grace: '1d' 用于已在途中的邮件)。之后该发件人的任何营销邮件都会使 after_unsubscribe 不通过。0.10.0 新增
markResubscribed(address, options?) Labs。 该地址重新订阅:结束监控。0.10.0 新增
recordConsent(address, consent) Labs。 记录账号在注册时同意的内容(marketing、prechecked、doubleOptIn),用于按国家判断营销规则。consent(address) 读取它。0.10.0 新增
consent(address) Labs。 记录的账号同意内容。0.10.0 新增
recordCouponResult(address, result, options?) Labs。 在结账时使用优惠券后(staging,测试支付),记录 accepted、rejected 或 error 以及应用的 message(code、oneTime);check 是 coupon_result 的结论。0.10.0 新增
checkSequence(address, steps, options?) Labs。 欢迎系列或滴灌营销在 since 之后是否按顺序准时到达;每一步可以给出 subject、category、type、after(‘1d’)和 within。0.10.0 新增
offers(address) Labs。 每个发件人的优惠券,以及每张发给了地址的哪个版本(原样、大写、+tag);当同一个人两次获得优惠时,passed 为 false。0.10.0 新增
checkUnsubscribe(address) Labs。 每次退订以及之后收到的营销邮件;只要收到任何一封,passed 就为 false。0.10.0 新增
forTest(name), recordTest(result), recordTestTotals(totals) 一个在每次调用中标明测试名称的客户端,以及用于运行报告的测试结果;proxymail-qa/playwright 及其报告器会使用它们。0.8.0 新增(详情)
formValues({ fields, address?, age?, country?, … }) 注册表单每个字段该输入或选择什么,基于为此新建的地址。0.7.0 新增(详情)
checkEmail(address, options?) 检查最新的匹配邮件(或 emailId):SPF、DKIM、DMARC、本地和普通 http 链接、链接指向哪里(expectHost)、标明的过期时间(从 0.10.0 起还包括优惠券何时结束)、纯文本部分、大小以及纯图片邮件。从 0.7.0 起,检查应包含的短语(expectText)和语言(expectLanguage);从 0.10.0 起,检查它在 since 之后多久到达(expectWithin: '5m')及其附件(expectAttachment: ['.pdf'])。营销邮件还会检查 list_unsubscribe、one_click_unsubscribe、unsubscribe_link 和 preheader,每封邮件都会检查 harmful_attachment 和 spam_risk(估计它是否会进入垃圾邮件)。只返回结论,从不返回内容。0.4.0 新增
checkDelivery(address) 哪些邮件送达了该地址、哪些被拒收,无需等待
variants(address) 地址的少见变体(仍能收到其邮件)以及无效变体(详情)
nameVariants({ field?, maxLength? }) 用于姓名字段的刁钻名字:应接受的(撇号、重音、其他文字)、应拒绝的、应作为文本存储的 HTML 或 SQL,以及各应用处理不一的。0.11.0 新增(详情)
passwordVariants({ minLength?, maxLength?, symbols? }) 符合字段规则的刁钻密码:最短和最长、每类符号、空格、非 ASCII,以及各应用处理不一的;每次调用都重新生成。0.11.0 新增(详情)
getAddress(address), listAddresses({ limit?, before?, tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment? }) 读取地址,或查找之前创建的地址:给出的每个筛选条件都必须匹配
updateAddress(address, patch) 更改 label、notes、tags、first_name、last_name、username、date_of_birth 或 country;null 清除其中一项
setPassword(address, { password?, label? }) 在应用更改或重置密码后,保存测试账号当前使用的密码。如果不提供,会生成一个强密码并只返回一次
savedPassword(address, label?) 读取已保存的密码。需要具有“Can read saved passwords”权限的密钥;每次读取都会被记录
signInAccount({ site?, environment?, tags? }) 一次调用获取用于登录的已保存测试账号及其密码:优先返回在此站点用过的账号;没有合适的则返回 null。需要具有“可读取已保存密码”权限的密钥。0.9.0 新增(详情)
markDevHost(host) 将 localhost:3000 这样的主机标记为项目的开发环境,这样指向它的链接能通过 checkEmail。0.9.0 新增
deleteAddress(address) 删除地址及其邮件
project() 密钥所属的项目:邮件域名、邮件前缀和限额
updateProject({ name?, website?, environments?, emailPrefix?, mailDomain? }) 修改项目,与项目设置中相同;null 会清除 website 或 emailPrefix。需要项目负责人,或组织所有者或管理员
checkOutAccount({ tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment?, minutes? }), extendCheckout(account, minutes?), returnAccount(account, { addTags?, removeTags? }), withAccount(options, fn) 账号池:借用一个状态已知、其他测试都没在用的账号,用完归还。0.6.0 新增(详情)。从 0.7.0 起,withAccount 接受 create、check 和 reset 钩子:状态已知的账号
startRun({ name?, source?, meta? }), finishRun({ failed? }), runReport(), addRunNote(text, { failed? }), getRun(), listRuns() 一次运行:此客户端的调用会把看到的内容加入其中,生成拉取请求上的报告。0.5.0 新增

每个方法都接受普通地址或它的变体。等待 +tag 地址时只会找到发往该标签的邮件,因此一个地址可以用于整个测试运行:见在 CI 中运行。

标有 Labs 的方法属于营销和优惠邮件,这是一项 Labs 功能,由密钥的创建者在应用的 Account settings → Labs 中开启;开启前,调用会抛出 403 错误并说明此情况。checkEmail 不包含已关闭的 Labs 功能的检查。

formValues 会为表单的每个字段给出要输入或选择的值,来自为此新建的地址:符合字段规则的用户名和密码、符合字段格式的出生日期、下拉列表中提供的国家。在 Playwright 中,从页面读取字段并逐一填写:

const form = page.locator('form#sign-up');
const fields = await form.locator('input, select').evaluateAll((elements) =>
elements.map((el, i) => {
const input = el as HTMLInputElement & HTMLSelectElement;
input.dataset.field = String(i);
return {
id: String(i),
type: input.tagName === 'SELECT' ? 'select' : input.type,
name: input.name,
label: input.labels?.[0]?.textContent ?? input.getAttribute('aria-label') ?? '',
placeholder: input.placeholder,
autocomplete: input.getAttribute('autocomplete') ?? '',
pattern: input.pattern || undefined,
minLength: input.minLength > 0 ? input.minLength : undefined,
maxLength: input.maxLength > 0 ? input.maxLength : undefined,
options: input.tagName === 'SELECT' ? Array.from(input.options, (o) => ({ value: o.value, label: o.text })) : undefined,
};
}),
);
const { address, values } = await qa.formValues({ fields });
for (const { id, kind, value } of values) {
if (value === null) continue;
const field = form.locator(`[data-field="${id}"]`);
if (await field.evaluate((el) => el.tagName === 'SELECT')) await field.selectOption(value);
else await field.fill(value);
}

传入 address 使用之前创建的账号,传入 age 或 country 创建新账号。详见 REST API 指南。

检查没有邮件到达,或只到达一封

Section titled “检查没有邮件到达,或只到达一封”
// An unsubscribed user gets no newsletter.
const sent = new Date();
await sendNewsletter();
await qa.expectNoEmail(unsubscribed.address, { since: sent, timeout: 20 });
// A sign-up sends one verification email, not two.
await qa.waitForLink(address, { since: submitted });
await new Promise((resolve) => setTimeout(resolve, 10_000)); // time for a duplicate
expect((await qa.countEmails(address, { since: submitted, type: 'link' })).count).toBe(1);

两者都不返回邮件内容,只返回每封邮件的到达时间、发件人,以及其中验证码或链接的类型。详见 REST API 指南。

await qa.waitForLink(address, { since: submitted });
const result = await qa.checkEmail(address, { since: submitted, type: 'link', expectHost: 'staging.example.com' });
expect(result.failed).toEqual([]); // e.g. ['local_links'] when the link goes to localhost
// A localised email: the right language, with the user's name in it.
const german = await qa.checkEmail(address, { since: submitted, expectLanguage: 'de', expectText: [persona.first_name] });
expect(german.failed).toEqual([]);

每项检查会通过、失败、警告或跳过,并附一行原因;各项含义见 REST API 指南。要测试在你本机运行的应用?执行一次 await qa.markDevHost('localhost:3000'),指向它的链接就能通过 local_links;指向其他 localhost 的链接仍会失败。

为账号标记其状态,然后让每个测试借用一个。withAccount 借出一个账号,运行你的函数,并始终归还;返回 { addTags, removeTags } 可记录测试做了哪些改动:

await qa.withAccount({ tags: ['verified', 'premium'] }, async (account) => {
const { secret } = await qa.savedPassword(account.address);
await signIn(page, account.address, secret);
await cancelSubscription(page);
return { addTags: ['cancelled'], removeTags: ['premium'] };
});

账号借出期间(默认 10 分钟,除非传入 minutes),其他测试不会拿到同一个账号;一直未归还的账号会在到期后自动释放。

当测试需要在你的应用中处于特定状态的账号(已验证、高级套餐、没有订单)时,给 withAccount 提供三个钩子。这些钩子由你的测试代码用它自己对应用的访问权限运行;Proxy Mail QA 从不登录你的应用。这里 app 代表你自己与应用交互的代码:管理 API、数据库种子数据或 UI。

await qa.withAccount(
{
tags: ['verified', 'premium'],
// When none is free: sign up with the new address, verify it, upgrade it.
create: async (account) => {
const submitted = new Date();
await app.signUp(account.address, account.password);
const { value: link } = await qa.waitForLink(account.address, { type: 'verify', since: submitted });
await app.open(link);
await app.admin.setPlan(account.address, 'premium');
},
// Is it still in that state? Return false or throw if not.
check: async (account) => (await app.admin.user(account.address)).plan === 'premium',
// After the test: put it back.
reset: async (account) => app.admin.resetUser(account.address, { plan: 'premium', orders: [] }),
},
async (account) => {
// …the test…
},
);
  • check 在测试拿到账号之前运行。如果失败,就运行 reset 并再次检查;仍失败则尝试下一个账号(最多 attempts 个,默认 3 个),失败的账号会失去池标签并被标记为 check-failed。
  • create 在没有匹配账号或所有匹配账号都已借出时运行。新地址从一开始就为此测试保留,在 create 和 check 成功后获得标签。传入 newAccount 设置年龄、标注或域名;国家取自筛选条件。
  • reset 在测试之后运行,无论测试是否通过。如果 reset 失败,账号会失去池标签并被标记为 needs-reset,这样其他测试不会拿到状态未知的账号。

在一次运行中,新建账号、检查失败和重置失败都会在报告中各加一行。

运行开启期间的调用会把看到的内容加入其中:地址、哪些邮件到达及在 since 之后多久到达、验证码、每个链接指向哪里、各项检查、退订、优惠和优惠券检查、失败的测试,以及始终未到的邮件。在 GitHub Actions 中,Proxy Mail QA action 会开始和结束运行,并把报告发布到拉取请求上;客户端会从 PROXYMAIL_QA_RUN 获取运行,无需修改你的测试。在其他环境中,自己运行:

const run = await qa.startRun({ name: 'Sign-up tests', meta: { branch: process.env.BRANCH } });
// …the tests, using qa…
await qa.finishRun({ failed: testsFailed });
console.log(await qa.runReport(run.id)); // Markdown, for a pull request comment

把运行 id 作为 PROXYMAIL_QA_RUN 传给其他进程,它们就会加入该运行。

要显示每封邮件来自哪个测试,为每个测试使用一个客户端:qa.forTest('sign-up › sends one code') 会在每次调用中写明测试名。在 Playwright 中,proxymail-qa/playwright 为每个测试提供一个这样做的 qa fixture,proxymail-qa/playwright/reporter 会在运行中记录失败和不稳定的测试及总数:见在 CI 中运行。其他测试运行器可以用 qa.recordTest({ name, status, durationMs, error }) 和 qa.recordTestTotals({ passed, failed, skipped, flaky }) 自行发送结果。0.8.0 新增。

调用失败时会抛出 ProxyMailQAError,包含 HTTP status、code 和可读的消息。等待没有结果时,消息还会尽 Proxy Mail QA 所知说明原因(error.hint),例如应用的邮件服务商只向已验证地址发信;error.refused 会列出因过大、超出限额或来自 Proxy Mail QA 不支持的网站而被拒收的邮件(refused[].site 会写明):

import { ProxyMailQAError } from 'proxymail-qa';
try {
await qa.waitForCode(address, { since: submitted });
} catch (error) {
if (error instanceof ProxyMailQAError && error.status === 404) console.log(error.hint);
throw error;
}

在应用能向任意地址发信之前,重试没有用。见测试邮件收不到?

测试和智能体可以回到之前创建的账号,而不必再建一个。给出的每个筛选条件都必须匹配;年龄按今天计算的整岁,依据角色的出生日期;search 会在地址、标注、备注、姓名和用户名中查找。

// An under-18 in the UK, made for the age-gate tests, with a saved password.
const [teen] = await qa.listAddresses({ tags: ['age-gate'], country: 'GB', maxAge: 17, hasPassword: true });
// Record where an account is used, then sign in with it again later: the
// account used on this site first, with its password (needs a key with
// "Can read saved passwords").
await qa.createAddress({ password: true, site: 'staging.example.com', environment: 'staging' });
const account = await qa.signInAccount({ site: 'staging.example.com', tags: ['verified'] });

对本页有疑问?

可以用任何语言提问。文档可以立即回答,你也可以把问题发给 Proxy Mail QA 团队,他们会用英文回复。