Detox と Appium
英語から自動翻訳されています。
英語で読むヘルパーを追加する
Section titled “ヘルパーを追加する”このファイルをテストに追加します(Node 18 以降)。使うのは
fetchだけです:proxymail-qa.js // Disposable inboxes for Detox and Appium tests (Node 18 or later): create a// test address, sign up with it in your app, then wait for the code or link// from the email it gets, and check the email itself.// Guide: https://proxymail.qa/docs/detox-appium//// The API key comes from the test run's environment (PROXYMAIL_QA_API_KEY),// never from the app. With PROXYMAIL_QA_RUN set (the GitHub Action sets it),// the calls add to that run, named by test: Jest's (and so Detox's) current// test, or the name given to setTest.const BASE_URL = process.env.PROXYMAIL_QA_API_URL ?? 'https://api.proxymail.qa/v1';let testName;async function send(method, path, body) {const key = process.env.PROXYMAIL_QA_API_KEY;if (!key) throw new Error('Set PROXYMAIL_QA_API_KEY for the test run.');const run = process.env.PROXYMAIL_QA_RUN;const test = testName ?? globalThis.expect?.getState?.().currentTestName;const response = await fetch(BASE_URL + path, {method,headers: {Authorization: `Bearer ${key}`,'Content-Type': 'application/json',// Percent-encoded: a test's name can hold characters headers can't....(run ? { 'Proxy-Mail-QA-Run': run, ...(test ? { 'Proxy-Mail-QA-Test': encodeURIComponent(test) } : {}) } : {}),},body: body && JSON.stringify(body),signal: AbortSignal.timeout(40_000),});const json = await response.json();// A wait that finds nothing says why: read the hint before retrying.if (!response.ok) throw Object.assign(new Error([json.message, json.hint].filter(Boolean).join(' ')), { status: response.status });return json;}const at = (address, path, query = {}) => `/addresses/${encodeURIComponent(address)}/${path}?${new URLSearchParams(query)}`;/** The test the next calls belong to, in the run's report. Under Jest, the current test is used without it. */export const setTest = (name) => {testName = name;};/*** A new test address, with a password for the sign-up form: { address, password }. Options:* environment (such as 'staging') records where it's used; age ('adult', 'under18', 'under13'…) and* country ('GB') give the persona a date of birth and a country.*/export const createAddress = ({ environment, age, country } = {}) => send('POST', '/addresses', { password: true, persona: true, environment, age, country });const waitFor = async (kind, address, since, query) => (await send('GET', at(address, kind, { since: since.toISOString(), ...query }))).value;/*** The code from the newest email received since `since`, waiting up to 30 seconds. `unused: true` skips a code* already used. `type: 'coupon'` waits for a coupon or promo code instead (after signing up for an offer), 'any' for either.*/export const waitForCode = (address, since, unused = false, type) =>waitFor('code', address, since, { ...(unused ? { unused: 'true' } : {}), ...(type ? { type } : {}) });/*** The link from the newest email received since `since`: verify, magic_signin, invite, reset or any.* `app: true` asks for the link that opens the app (myapp://…) when the email also has a web one.*/export const waitForLink = (address, since, type = 'verify', app) => waitFor('link', address, since, { type, ...(app === undefined ? {} : { app: String(app) }) });/** How many emails arrived since `since`, to check the app sends exactly one. */export const countEmails = async (address, since) => (await send('GET', at(address, 'count', { since: since.toISOString() }))).count;/*** The checks the newest email since `since` fails: SPF, DKIM, DMARC, links to localhost or plain http,* a stated expiry, its size and more. Empty when it passes. `expectHost` is the domain its link should* open, such as your app's Universal Link or App Link domain.*/export const checkEmail = async (address, since, expectHost) =>(await send('GET', at(address, 'checks', { since: since.toISOString(), ...(expectHost ? { expect_host: expectHost } : {}) }))).failed;/*** Resolves if no email arrives within `timeout` seconds (at most 30); throws, saying what came, as soon as one does.* `category: 'marketing'` watches only for offers and newsletters, so receipts can still arrive.*/export const expectNoEmail = async (address, since, timeout = 30, category) => {await send('GET', at(address, 'none', { since: since.toISOString(), timeout: String(timeout), ...(category ? { category } : {}) }));};/*** A saved test account to sign in with, and its password: one with every tag given that no test has* checked out, the one that got mail most recently first. null when none fits. Needs an API key* allowed to read saved passwords.*/export const signInAccount = async ({ tags = [], environment } = {}) => {try {return await send('POST', '/addresses/sign-in', { tags, environment });} catch (err) {if (err.status === 404) return null;throw err;}};/*** Borrows an account with every tag given (verified, premium…) that no other test has, for `minutes`,* with its saved password when the key may read it: { address, password, tags, checkout }. Give it* back with returnAccount.*/export const checkOutAccount = async (tags, minutes = 10) => {const account = await send('POST', '/addresses/checkout', { tags, minutes });try {account.password = (await send('GET', at(account.address, 'password'))).secret;} catch (err) {if (err.status !== 403 && err.status !== 404) throw err;}return account;};/** Gives a borrowed account back, recording what the test changed (tags to add and remove). */export const returnAccount = (account, { addTags = [], removeTags = [] } = {}) =>send('POST', `/addresses/${encodeURIComponent(account.address)}/return`, { token: account.checkout.token, add_tags: addTags, remove_tags: removeTags });/*** After applying a coupon at checkout in the app (the store's sandbox or the app's test mode, never a real card),* records what happened: 'accepted', 'rejected' or 'error', with the app's message. `code` is the coupon applied* (default: the newest at the address); `oneTime` marks a one-per-person offer. Resolves to the coupon_result* verdict: 'pass', 'warn' or 'fail'.*/export const recordCouponResult = async (address, result, { code, message, oneTime = false } = {}) =>(await send('POST', `/addresses/${encodeURIComponent(address)}/coupon-result`, { result, code, message, one_time: oneTime })).check?.status ?? 'skip';/*** Records what the account agreed to at sign-up: `marketing` 'opted_in' or 'declined', `prechecked` when the box was* ticked as the screen loaded, and `doubleOptIn` 'confirmed' or 'pending'. The rules where the persona lives then* judge the marketing it gets.*/export const recordConsent = (address, marketing, { prechecked, doubleOptIn } = {}) =>send('PUT', `/addresses/${encodeURIComponent(address)}/consent`, { marketing, prechecked, double_opt_in: doubleOptIn });API キー を設定する
Section titled “API キー を設定する”テストを実行する環境で、CI のシークレットから
PROXYMAIL_QA_API_KEYを設定します。
import { createAddress, waitForCode, waitForLink } from './proxymail-qa.js';
it('sends a working code', async () => { const { address, password } = await createAddress(); await device.launchApp();
const submitted = new Date(); await element(by.id('email')).typeText(address); await element(by.id('password')).typeText(password); await element(by.text('Sign up')).tap();
const code = await waitForCode(address, submitted); await element(by.id('code')).typeText(code);});確認リンクやマジックリンクの場合は、サインアップ後にアプリで開きます。メールにウェブ用のリンクとアプリを開くリンク(myapp://…)の両方がある場合、アプリ用のリンクを使うには 4 番目の引数に true を渡します:
const link = await waitForLink(address, submitted, 'verify', true);await device.openURL({ url: link });Appium (WebdriverIO)
Section titled “Appium (WebdriverIO)”import { createAddress, waitForCode, waitForLink } from './proxymail-qa.js';
it('sends a working code', async () => { const { address, password } = await createAddress(); const submitted = new Date(); await $('~email').setValue(address); await $('~password').setValue(password); await $('~Sign up').click();
const code = await waitForCode(address, submitted); await $('~code').setValue(code);});確認リンクやマジックリンクの場合は、サインアップ後にアプリで開きます:
const link = await waitForLink(address, submitted);await driver.execute('mobile: deepLink', { url: link, package: 'com.example.app' }); // Androidawait driver.execute('mobile: deepLink', { url: link, bundleId: 'com.example.app' }); // iOSコードだけでなくメールもチェックする
Section titled “コードだけでなくメールもチェックする”import { checkEmail, countEmails } from './proxymail-qa.js';
expect(await countEmails(address, submitted)).toBe(1);expect(await checkEmail(address, submitted, 'links.example.com')).toEqual([]);checkEmail は、メールが不合格になったチェックを返します。SPF、DKIM、DMARC、localhost や平文の http へのリンク、リンク先(3 番目の引数は、アプリのリンクが使うドメイン)、記載された有効期限、サイズなどです(各チェックの意味)。expectNoEmail(address, since) は何も届かない場合にのみ解決し、「配信停止したユーザーには何も届かない」ことを確認できます。4 番目の引数に 'marketing' を渡すとマーケティングメールだけを監視するため、領収書は届いてもかまいません。再送後は、waitForCode(address, submitted, true) が使用済みのコードをスキップします。オファーに登録した後は、waitForCode(address, submitted, false, 'coupon') が代わりにクーポンやプロモーションコードを待ちます。チェックアウトで適用した後(ストアのサンドボックスやアプリのテストモードを使い、実際のカードは使わないでください)、recordCouponResult(address, 'accepted', { message }) がアプリの動作を記録し、coupon_result の判定 'pass'、'warn'、'fail' のいずれかで解決します。1 人 1 回のオファーには oneTime: true を追加します。recordConsent(address, 'declined', { prechecked: false }) はアカウントが同意した内容を記録するため、checkEmail はペルソナが住む地域のルールに沿ってマーケティングを判定します。
createAddress({ environment: 'staging', age: 'under18', country: 'GB' }) は、アカウントがどの環境用かを記録し、入力を求めるフォーム向けに、ペルソナに生年月日と国を設定します。
保存したアカウントでサインインする、または借りる
Section titled “保存したアカウントでサインインする、または借りる”サインインのテストでは、signInAccount({ tags: ['verified'] }) が保存済みのテスト用アカウントとそのパスワードを返し、合うものがなければ null を返します。テストを並列で実行する場合、checkOutAccount(['verified']) は他のテストが使っていないアカウントをパスワード付きで、返却されるまで貸し出します:
const account = await checkOutAccount(['verified']);try { // Sign in with account.address and account.password.} finally { await returnAccount(account);}どちらも保存済みのパスワードを読むため、キーには Can read saved passwords 権限が必要です(Project settings → API keys)。アプリでの状態をアカウントにタグ付けするとプールを作れます。アカウントプール を参照してください。
プルリクエストへのレポート
Section titled “プルリクエストへのレポート”ジョブで GitHub Action を使う場合は、テストにも PROXYMAIL_QA_RUN を設定します(アクションはその後のステップにだけ設定します)。Detox が使う Jest では、各呼び出しに現在のテスト名が付きます。WebdriverIO では、フックで自分で名前を付けます:
import { setTest } from './proxymail-qa.js';
beforeEach(function () { setTest(this.currentTest.fullTitle());});このページについて質問がありますか?
質問はどの言語でもかまいません。ドキュメントからすぐに回答を得ることも、Proxy Mail QA チームに質問を送ることもできます。チームからの返信は英語です。