Node.js 客户端
本页由英文自动翻译。
阅读英文版proxymail-qa 为 JavaScript 和 TypeScript 封装了 REST API:可用于 Playwright、Cypress 任务、Vitest、Jest、Detox 或普通脚本。它没有依赖,自带类型,可在 Node 18 或更高版本上运行(或任何支持 fetch 的环境)。源代码位于 github.com/aaquib90/proxymail-qa-js,每个版本都从 GitHub Actions 发布,并附带 npm 来源证明。
- npm install --save-dev proxymail-qapnpm add -D proxymail-qayarn add -D proxymail-qabun add -d proxymail-qa
设置 API 密钥
Section titled “设置 API 密钥”创建一个 API 密钥 并设为
PROXYMAIL_QA_API_KEY,或直接传入。创建地址并等待
Section titled “创建地址并等待”创建一个地址,在你的应用中用它注册,然后等待验证码或链接。
等待只返回验证码或链接本身,以及发送它的服务和过期时间,从不返回邮件正文。一次等待最长持续 30 秒,更长的
timeout会重复等待。import { ProxyMailQA } from 'proxymail-qa';const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEYconst { 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 功能的检查。
注册表单的填写值
Section titled “注册表单的填写值”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 duplicateexpect((await qa.countEmails(address, { since: submitted, type: 'link' })).count).toBe(1);两者都不返回邮件内容,只返回每封邮件的到达时间、发件人,以及其中验证码或链接的类型。详见 REST API 指南。
检查邮件的发送和构建
Section titled “检查邮件的发送和构建”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 的链接仍会失败。
用于并行测试的账号池
Section titled “用于并行测试的账号池”为账号标记其状态,然后让每个测试借用一个。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),其他测试不会拿到同一个账号;一直未归还的账号会在到期后自动释放。
状态已知的账号
Section titled “状态已知的账号”当测试需要在你的应用中处于特定状态的账号(已验证、高级套餐、没有订单)时,给 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;}在应用能向任意地址发信之前,重试没有用。见测试邮件收不到?
重新找到测试账号
Section titled “重新找到测试账号”测试和智能体可以回到之前创建的账号,而不必再建一个。给出的每个筛选条件都必须匹配;年龄按今天计算的整岁,依据角色的出生日期;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 团队,他们会用英文回复。