Maestro
本页由英文自动翻译。
阅读英文版一个脚本即可覆盖原生 iOS 和 Android、React Native 以及 Flutter 应用,因为 Maestro 从外部驱动应用。把它保存在你的流程旁边,命名为
proxymail.js:proxymail.js // Proxy Mail QA for Maestro flows: works for native iOS and Android, React// Native and Flutter apps. Guide: https://proxymail.qa/docs/maestro//// - runScript: { file: proxymail.js, env: { ACTION: create } }// output.address, output.password, output.since. Add ENVIRONMENT// (staging), AGE (adult, under18…) or COUNTRY (GB) to set them.// - runScript: { file: proxymail.js, env: { ACTION: code } } (or link)// output.code (or output.link), for mail received since output.since.// Add APP_LINK: true for the link that opens the app (myapp://…) when// the email also has a web one, UNUSED: true to skip a used code, and// TYPE: coupon for a coupon or promo code (any for either), or for a// link, TYPE: unsubscribe for the email's unsubscribe link.// - runScript: { file: proxymail.js, env: { ACTION: count } }// output.count, the emails received since output.since// - runScript: { file: proxymail.js, env: { ACTION: check, EXPECT_HOST: links.example.com } }// fails the flow if the newest email fails a check (SPF, DKIM, DMARC,// local or http links, where its link goes…); output.failed lists them// - runScript: { file: proxymail.js, env: { ACTION: none } }// fails the flow if an email arrives within 30 seconds (TIMEOUT);// CATEGORY: marketing watches only for offers and newsletters// - runScript: { file: proxymail.js, env: { ACTION: signin, TAGS: verified } }// output.address and output.password of a saved test account; needs a// key allowed to read saved passwords// - runScript: { file: proxymail.js, env: { ACTION: checkout, TAGS: verified } }// borrows an account no other test has: output.address, output.password,// output.token. ACTION: return gives it back (ADD_TAGS, REMOVE_TAGS).// - runScript: { file: proxymail.js, env: { ACTION: coupon_result, RESULT: accepted } }// after applying the coupon at checkout (the store's sandbox, never a// real card): RESULT is accepted, rejected or error; add MESSAGE (what// the app said), CODE and ONE_TIME: true. output.verdict; fails the// flow if the coupon_result check fails.// - runScript: { file: proxymail.js, env: { ACTION: consent, MARKETING: declined } }// records what the account agreed to at sign-up: MARKETING opted_in or// declined; add PRECHECKED: true when the box started ticked and// DOUBLE_OPT_IN: pending or confirmed.//// The API key comes from the run: maestro test -e PROXYMAIL_QA_API_KEY=... flow.yaml// Add -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN to add the calls to a run (the// GitHub Action sets it), and TEST: a name, in env, to name the step.const API = 'https://api.proxymail.qa/v1';const headers = { Authorization: 'Bearer ' + PROXYMAIL_QA_API_KEY, 'Content-Type': 'application/json' };if (typeof PROXYMAIL_QA_RUN !== 'undefined' && PROXYMAIL_QA_RUN) {headers['Proxy-Mail-QA-Run'] = PROXYMAIL_QA_RUN;// Percent-encoded: a test's name can hold characters headers can't.if (typeof TEST !== 'undefined' && TEST) headers['Proxy-Mail-QA-Test'] = encodeURIComponent(TEST);}function call(method, path, body, extra) {const options = { headers: extra || headers };if (body) options.body = JSON.stringify(body);const response = method === 'POST' ? http.post(API + path, options) : method === 'PUT' ? http.put(API + path, options) : http.get(API + path, options);return { ok: response.ok, status: response.status, body: response.body ? json(response.body) : {} };}function fail(response) {// When nothing arrives, the hint says why (an email provider in sandbox mode, say).throw new Error('Proxy Mail QA: ' + (response.body.message || response.status) + ' ' + (response.body.hint || ''));}function tags(value) {return String(value).split(',').map(function (tag) { return tag.trim(); }).filter(function (tag) { return tag; });}function account(body) {output.address = body.address;output.password = body.password;output.since = new Date().toISOString();}const at = typeof output.address === 'undefined' ? '' : '/addresses/' + encodeURIComponent(output.address) + '/';const since = '?since=' + encodeURIComponent(output.since);if (ACTION === 'create') {const body = { password: true, persona: true };if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;if (typeof AGE !== 'undefined') body.age = AGE;if (typeof COUNTRY !== 'undefined') body.country = COUNTRY;const created = call('POST', '/addresses', body);if (!created.ok) fail(created);account(created.body);} else if (ACTION === 'count') {const counted = call('GET', at + 'count' + since);if (!counted.ok) fail(counted);output.count = counted.body.count;} else if (ACTION === 'check') {const host = typeof EXPECT_HOST !== 'undefined' ? '&expect_host=' + encodeURIComponent(EXPECT_HOST) : '';const checked = call('GET', at + 'checks' + since + host);if (!checked.ok) fail(checked);output.failed = checked.body.failed.join(', ');const failures = checked.body.checks.filter(function (c) { return c.status === 'fail'; }).map(function (c) { return c.check + ': ' + c.detail; });if (failures.length) throw new Error('Proxy Mail QA: the email failed ' + failures.join(' '));} else if (ACTION === 'none') {const timeout = typeof TIMEOUT !== 'undefined' ? TIMEOUT : 30;const category = typeof CATEGORY !== 'undefined' ? '&category=' + encodeURIComponent(CATEGORY) : '';const watched = call('GET', at + 'none' + since + '&timeout=' + timeout + category);if (!watched.ok) fail(watched);} else if (ACTION === 'signin') {const body = { tags: typeof TAGS !== 'undefined' ? tags(TAGS) : [] };if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;const found = call('POST', '/addresses/sign-in', body);if (!found.ok) fail(found);account(found.body);} else if (ACTION === 'checkout') {const held = call('POST', '/addresses/checkout', { tags: tags(TAGS) });if (!held.ok) fail(held);// Its saved password, when the key may read it.const secret = call('GET', '/addresses/' + encodeURIComponent(held.body.address) + '/password');account({ address: held.body.address, password: secret.ok ? secret.body.secret : undefined });output.token = held.body.checkout.token;} else if (ACTION === 'consent') {const body = { marketing: MARKETING };if (typeof PRECHECKED !== 'undefined') body.prechecked = String(PRECHECKED) === 'true';if (typeof DOUBLE_OPT_IN !== 'undefined') body.double_opt_in = DOUBLE_OPT_IN;const recorded = call('PUT', at + 'consent', body);if (!recorded.ok) fail(recorded);} else if (ACTION === 'coupon_result') {const body = { result: RESULT, one_time: typeof ONE_TIME !== 'undefined' && String(ONE_TIME) === 'true' };if (typeof CODE !== 'undefined') body.code = CODE;if (typeof MESSAGE !== 'undefined') body.message = MESSAGE;const recorded = call('POST', at + 'coupon-result', body);if (!recorded.ok) fail(recorded);const check = recorded.body.check || { status: 'skip', detail: '' };output.verdict = check.status;if (check.status === 'fail') throw new Error('Proxy Mail QA: ' + check.detail);} else if (ACTION === 'return') {const body = { token: output.token };if (typeof ADD_TAGS !== 'undefined') body.add_tags = tags(ADD_TAGS);if (typeof REMOVE_TAGS !== 'undefined') body.remove_tags = tags(REMOVE_TAGS);const returned = call('POST', at + 'return', body);if (!returned.ok) fail(returned);} else {// Each request waits up to 10 seconds; four tries cover the email taking up to 40.const kind = ACTION === 'link' ? 'link' : 'code';const app = kind === 'link' && typeof APP_LINK !== 'undefined' && String(APP_LINK) === 'true' ? '&app=true' : '';const unused = typeof UNUSED !== 'undefined' && String(UNUSED) === 'true' ? '&unused=true' : '';const type = typeof TYPE !== 'undefined' && TYPE ? '&type=' + encodeURIComponent(TYPE) : '';let found = null;for (let i = 0; i < 4 && !found; i++) {// Only the last try's "nothing arrived" counts in a run's report.const retry = JSON.parse(JSON.stringify(headers));if (i < 3) retry['Proxy-Mail-QA-Run-Retry'] = 'true';found = call('GET', at + kind + since + '&timeout=10' + app + unused + type, null, retry);if (!found.ok && i < 3) found = null;}if (!found.ok) throw new Error('Proxy Mail QA: no ' + kind + ' arrived. ' + (found.body.hint || ''));output[kind] = found.body.value;}在流程中使用
Section titled “在流程中使用”然后在流程中使用它:创建地址,用它注册,并填写验证码。
signup.yaml appId: com.example.app---- launchApp- runScript:file: proxymail.jsenv:ACTION: create- tapOn: "Email"- inputText: ${output.address}- tapOn: "Password"- inputText: ${output.password}- tapOn: "Sign up"- runScript:file: proxymail.jsenv:ACTION: code- inputText: ${output.code}用你的密钥运行
Section titled “用你的密钥运行”用你的 API 密钥 运行:
如果是链接而不是验证码,使用
ACTION: link,然后用- openLink: ${output.link}在应用中打开它。如果邮件中同时有网页链接和打开应用的链接(myapp://…),在脚本的env中添加APP_LINK: true来获取应用链接。maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY signup.yaml
检查邮件本身,而不只是验证码
Section titled “检查邮件本身,而不只是验证码”- runScript: file: proxymail.js env: ACTION: count- assertTrue: ${output.count == 1}- runScript: file: proxymail.js env: ACTION: check EXPECT_HOST: links.example.com当最新的邮件未通过检查时,ACTION: check 会让流程失败,并写明失败项:SPF、DKIM 和 DMARC,指向 localhost 或纯 http 的链接,链接指向哪里(EXPECT_HOST,你应用链接使用的域名),注明的有效期,邮件大小等(每项检查的含义)。ACTION: none 在 30 秒内收到邮件时让流程失败,用于验证“已退订用户收不到任何邮件”;添加 CATEGORY: marketing 可以只监控营销邮件,这样收据类邮件仍可到达。重新发送后,在 ACTION: code 上使用 UNUSED: true 会跳过已用过的验证码。注册某个优惠后,在 ACTION: code 上使用 TYPE: coupon 会改为等待它的优惠券或优惠码。在结账时使用后(使用商店沙盒,绝不使用真实卡片),ACTION: coupon_result 带 RESULT: accepted(或 rejected、error)、MESSAGE 和 ONE_TIME: true 会记录应用的处理结果,设置 output.verdict,并在 coupon_result 检查失败时让流程失败。ACTION: consent 带 MARKETING: declined(以及 PRECHECKED、DOUBLE_OPT_IN)会记录账号同意了什么,这样 ACTION: check 就能按用户画像所在地的规则判断其营销邮件。
在 ACTION: create 上,ENVIRONMENT: staging 记录账号所属的环境,AGE: under18 或 COUNTRY: GB 为其用户画像提供出生日期或国家,用于需要填写这些信息的表单。
以已保存的账号登录,或借用一个
Section titled “以已保存的账号登录,或借用一个”对于登录流程,ACTION: signin 带 TAGS: verified 会把 output.address 和 output.password 设为已保存测试账号的值。流程并行运行时,ACTION: checkout 带 TAGS: verified 会借出一个其他流程都没在用的账号及其密码,ACTION: return 会归还它(ADD_TAGS 和 REMOVE_TAGS 记录流程做了哪些更改):
- runScript: file: proxymail.js env: ACTION: checkout TAGS: verified# …sign in with ${output.address} and ${output.password}…- runScript: file: proxymail.js env: ACTION: return两者都会读取已保存的密码,所以密钥需要 Can read saved passwords 权限(Project settings → API keys)。按账号在应用中的状态给它们打标签,就能组成账号池:参见账号池。
pull request 上的报告
Section titled “pull request 上的报告”在作业中使用 GitHub Action 时,像传递密钥一样传递它启动的运行,并在每个脚本的 env 中添加 TEST 来命名步骤:
maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN signup.yaml对本页有疑问?
可以用任何语言提问。文档可以立即回答,你也可以把问题发给 Proxy Mail QA 团队,他们会用英文回复。