Aller au contenu
  1. Ajoutez ce fichier à vos tests (Node 18 ou ultérieur). Il n’utilise que 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 });
  2. Définissez PROXYMAIL_QA_API_KEY dans l’environnement qui exécute les tests, à partir des secrets de votre CI.

e2e/signup.test.js
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);
});

Pour un lien de vérification ou magique, ouvrez-le dans l’app après l’inscription. Si l’e-mail contient à la fois un lien web et un lien qui ouvre l’app (myapp://…), passez true en quatrième argument pour celui de l’app :

const link = await waitForLink(address, submitted, 'verify', true);
await device.openURL({ url: link });
test/signup.e2e.js
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);
});

Pour un lien de vérification ou magique, ouvrez-le dans l’app après l’inscription :

const link = await waitForLink(address, submitted);
await driver.execute('mobile: deepLink', { url: link, package: 'com.example.app' }); // Android
await driver.execute('mobile: deepLink', { url: link, bundleId: 'com.example.app' }); // iOS
import { checkEmail, countEmails } from './proxymail-qa.js';
expect(await countEmails(address, submitted)).toBe(1);
expect(await checkEmail(address, submitted, 'links.example.com')).toEqual([]);

checkEmail renvoie les vérifications que l’e-mail ne passe pas : SPF, DKIM et DMARC, liens vers localhost ou en http simple, destination de son lien (le troisième argument, le domaine qu’utilisent les liens de votre app), une expiration indiquée, sa taille et plus encore (signification de chaque vérification). expectNoEmail(address, since) ne se résout que si rien n’arrive, pour « un utilisateur désabonné ne reçoit rien » ; passez 'marketing' en quatrième argument pour ne surveiller que le marketing, afin que les reçus puissent encore arriver. Après un renvoi, waitForCode(address, submitted, true) ignore le code déjà utilisé. Après l’inscription à une offre, waitForCode(address, submitted, false, 'coupon') attend plutôt son coupon ou code promo. Après l’avoir appliqué au paiement (la sandbox de la boutique ou le mode test de l’app, jamais une vraie carte), recordCouponResult(address, 'accepted', { message }) enregistre ce qu’a fait l’app et se résout avec le verdict coupon_result, 'pass', 'warn' ou 'fail' ; ajoutez oneTime: true pour une offre limitée à une par personne. recordConsent(address, 'declined', { prechecked: false }) enregistre ce que le compte a accepté, pour que checkEmail juge son marketing selon les règles du lieu où vit le persona.

createAddress({ environment: 'staging', age: 'under18', country: 'GB' }) enregistre l’environnement auquel le compte est destiné et donne à son persona une date de naissance et un pays, pour les formulaires qui les demandent.

Se connecter avec un compte enregistré, ou en emprunter un

Section intitulée « Se connecter avec un compte enregistré, ou en emprunter un »

Pour un test de connexion, signInAccount({ tags: ['verified'] }) renvoie un compte de test enregistré et son mot de passe, ou null si aucun ne convient. Quand les tests tournent en parallèle, checkOutAccount(['verified']) en prête un qu’aucun autre test n’a, avec son mot de passe, jusqu’à ce que vous le rendiez :

const account = await checkOutAccount(['verified']);
try {
// Sign in with account.address and account.password.
} finally {
await returnAccount(account);
}

Les deux lisent des mots de passe enregistrés, donc la clé a besoin de l’autorisation Can read saved passwords (Project settings → API keys). Étiquetez les comptes selon leur état dans l’app pour créer des pools : voir un pool de comptes.

Avec l’Action GitHub dans le job, définissez aussi PROXYMAIL_QA_RUN pour les tests (l’action le définit pour les étapes qui la suivent). Sous Jest, qu’utilise Detox, chaque appel porte le nom du test en cours. Avec WebdriverIO, nommez-le vous-même dans un hook :

import { setTest } from './proxymail-qa.js';
beforeEach(function () {
setTest(this.currentTest.fullTitle());
});

Une question sur cette page ?

Posez votre question dans la langue de votre choix. La documentation peut y répondre tout de suite, ou vous pouvez l'envoyer à l'équipe Proxy Mail QA, qui répond en anglais.

Interroger la documentation