Detox und Appium
Automatisch aus dem Englischen übersetzt.
Auf Englisch lesenHelper hinzufügen
Abschnitt betitelt „Helper hinzufügen“Füge diese Datei zu deinen Tests hinzu (Node 18 oder neuer). Sie nutzt nur
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-Schlüssel setzen
Abschnitt betitelt „API-Schlüssel setzen“Setze
PROXYMAIL_QA_API_KEYin der Umgebung, in der die Tests laufen, aus den Secrets deines CI.
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);});Einen Bestätigungs- oder Magic Link öffnest du nach der Registrierung in der App. Wenn die E-Mail sowohl einen Weblink als auch einen Link zum Öffnen der App hat (myapp://…), übergib true als viertes Argument für den App-Link:
const link = await waitForLink(address, submitted, 'verify', true);await device.openURL({ url: link });Appium (WebdriverIO)
Abschnitt betitelt „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);});Einen Bestätigungs- oder Magic Link öffnest du nach der Registrierung in der App:
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' }); // iOSDie E-Mail prüfen, nicht nur den Code
Abschnitt betitelt „Die E-Mail prüfen, nicht nur den Code“import { checkEmail, countEmails } from './proxymail-qa.js';
expect(await countEmails(address, submitted)).toBe(1);expect(await checkEmail(address, submitted, 'links.example.com')).toEqual([]);checkEmail gibt die Prüfungen zurück, die die E-Mail nicht besteht: SPF, DKIM und DMARC, Links auf localhost oder einfaches http, wohin ihr Link führt (das dritte Argument, die Domain, die die Links deiner App nutzen), eine angegebene Ablaufzeit, ihre Größe und mehr (was jede Prüfung bedeutet). expectNoEmail(address, since) wird nur erfüllt, wenn nichts ankommt, für „ein abgemeldeter Nutzer bekommt nichts“; übergib 'marketing' als viertes Argument, um nur auf Marketing zu achten, damit Belege weiterhin ankommen können. Nach einem erneuten Senden überspringt waitForCode(address, submitted, true) den bereits genutzten Code. Nach der Anmeldung für ein Angebot wartet waitForCode(address, submitted, false, 'coupon') stattdessen auf dessen Gutschein- oder Promo-Code. Nachdem du ihn im Checkout angewendet hast (in der Sandbox des Shops oder im Testmodus der App, nie mit einer echten Karte), erfasst recordCouponResult(address, 'accepted', { message }), was die App getan hat, und liefert das Ergebnis coupon_result, 'pass', 'warn' oder 'fail'; füge oneTime: true für ein Angebot hinzu, das nur einmal pro Person gilt. recordConsent(address, 'declined', { prechecked: false }) erfasst, wozu das Konto zugestimmt hat, sodass checkEmail sein Marketing nach den Regeln am Wohnort der Persona bewertet.
createAddress({ environment: 'staging', age: 'under18', country: 'GB' }) erfasst, für welche Umgebung das Konto ist, und gibt seiner Persona ein Geburtsdatum und ein Land, für Formulare, die danach fragen.
Mit einem gespeicherten Konto anmelden oder eines ausleihen
Abschnitt betitelt „Mit einem gespeicherten Konto anmelden oder eines ausleihen“Für einen Anmeldetest gibt signInAccount({ tags: ['verified'] }) ein gespeichertes Testkonto und sein Passwort zurück oder null, wenn keines passt. Wenn Tests parallel laufen, leiht checkOutAccount(['verified']) eines aus, das kein anderer Test hat, mit seinem Passwort, bis du es zurückgibst:
const account = await checkOutAccount(['verified']);try { // Sign in with account.address and account.password.} finally { await returnAccount(account);}Beide lesen gespeicherte Passwörter, also braucht der Schlüssel die Berechtigung Can read saved passwords (Project settings → API keys). Markiere Konten in der App mit ihrem Zustand, um Pools zu bilden: siehe Konto-Pools.
Ein Bericht im Pull Request
Abschnitt betitelt „Ein Bericht im Pull Request“Mit der GitHub Action im Job setzt du PROXYMAIL_QA_RUN auch für die Tests (die Action setzt es für die Schritte nach ihr). Unter Jest, das Detox nutzt, wird jeder Aufruf nach dem aktuellen Test benannt. Mit WebdriverIO benennst du ihn selbst in einem Hook:
import { setTest } from './proxymail-qa.js';
beforeEach(function () { setTest(this.currentTest.fullTitle());});Eine Frage zu dieser Seite?
Frag in jeder Sprache. Die Doku kann dir sofort antworten oder deine Frage an das Team von Proxy Mail QA schicken, das auf Englisch antwortet.