コンテンツにスキップ
  1. Maestro はアプリを外部から操作するため、1 つのスクリプトでネイティブの iOS、Android、React Native、Flutter アプリに対応できます。フローと同じ場所に 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;
    }
  2. 次にフローで使います。アドレスを作成し、それでサインアップして、コードを入力します。

    signup.yaml
    appId: com.example.app
    ---
    - launchApp
    - runScript:
    file: proxymail.js
    env:
    ACTION: create
    - tapOn: "Email"
    - inputText: ${output.address}
    - tapOn: "Password"
    - inputText: ${output.password}
    - tapOn: "Sign up"
    - runScript:
    file: proxymail.js
    env:
    ACTION: code
    - inputText: ${output.code}
  3. API キー を指定して実行します:

    コードではなくリンクの場合は、ACTION: link を使い、次に - openLink: ${output.link} でアプリで開きます。メールに Web リンクとアプリを開くリンク(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 を付けると、代わりにクーポンやプロモーションコードを待ちます。チェックアウトで適用した後(ストアのサンドボックスで行い、実際のカードは使わないでください)、RESULT: accepted(または rejected、error)、MESSAGE、ONE_TIME: true を指定した ACTION: coupon_result がアプリの動作を記録し、output.verdict を設定して、coupon_result のチェックが失敗するとフローを失敗させます。MARKETING: declined(および PRECHECKED、DOUBLE_OPT_IN)を指定した ACTION: consent はアカウントが同意した内容を記録し、ACTION: check がペルソナの居住地のルールでマーケティングを判定します。

ACTION: create では、ENVIRONMENT: staging がアカウントの対象環境を記録し、AGE: under18 または COUNTRY: GB が、それらを求めるフォーム向けにペルソナの生年月日や国を設定します。

保存したアカウントでサインインする、または借りる

Section titled “保存したアカウントでサインインする、または借りる”

サインインフローでは、TAGS: verified を指定した ACTION: signin が、output.address と output.password を保存済みテスト用アカウントの値に設定します。フローを並列実行する場合は、TAGS: verified を指定した ACTION: checkout が、ほかのフローが使っていないアカウントをパスワードとともに貸し出し、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)。アプリでの状態をアカウントにタグ付けするとプールを作れます。アカウントプール を参照してください。

ジョブで 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 チームに質問を送ることもできます。チームからの返信は英語です。