Aller au contenu
  1. Un seul script couvre les apps natives iOS et Android, React Native et Flutter, car Maestro pilote l’app depuis l’extérieur. Enregistrez-le à côté de vos parcours sous le nom 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. Utilisez-le ensuite dans un parcours : créez l’adresse, inscrivez-vous avec, et saisissez le code.

    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. Exécutez-le avec votre clé d’API :

    Pour un lien au lieu d’un code, utilisez ACTION: link puis - openLink: ${output.link}, qui l’ouvre dans l’app. Si l’e-mail contient à la fois un lien web et un lien qui ouvre l’app (myapp://…), ajoutez APP_LINK: true à env du script pour obtenir celui de l’app.

    maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY signup.yaml
- 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 fait échouer le parcours, en nommant l’élément en échec, lorsque l’e-mail le plus récent échoue à une vérification : SPF, DKIM et DMARC, liens vers localhost ou en http simple, destination de son lien (EXPECT_HOST, le domaine utilisé par les liens de votre app), expiration indiquée, taille, etc. (signification de chaque vérification). ACTION: none le fait échouer si un e-mail arrive dans les 30 secondes, pour « un utilisateur désabonné ne reçoit rien » ; ajoutez CATEGORY: marketing pour ne surveiller que le marketing, afin que les reçus puissent encore arriver. Après un renvoi, UNUSED: true sur ACTION: code ignore le code déjà utilisé. Après l’inscription à une offre, TYPE: coupon sur ACTION: code attend plutôt son coupon ou code promo. Après l’avoir appliqué au paiement (la sandbox de la boutique, jamais une vraie carte), ACTION: coupon_result avec RESULT: accepted (ou rejected, error), MESSAGE et ONE_TIME: true enregistre ce qu’a fait l’app, définit output.verdict, et fait échouer le parcours si la vérification coupon_result échoue. ACTION: consent avec MARKETING: declined (et PRECHECKED, DOUBLE_OPT_IN) enregistre ce que le compte a accepté, afin que ACTION: check évalue son marketing selon les règles du lieu de résidence du persona.

Sur ACTION: create, ENVIRONMENT: staging enregistre l’environnement auquel le compte est destiné, et AGE: under18 ou COUNTRY: GB donnent à son persona une date de naissance ou 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 parcours de connexion, ACTION: signin avec TAGS: verified définit output.address et output.password sur ceux d’un compte de test enregistré. Lorsque des parcours s’exécutent en parallèle, ACTION: checkout avec TAGS: verified prête un compte qu’aucun autre parcours n’utilise, avec son mot de passe, et ACTION: return le rend (ADD_TAGS et REMOVE_TAGS enregistrent ce que le parcours a modifié) :

- runScript:
file: proxymail.js
env:
ACTION: checkout
TAGS: verified
# …sign in with ${output.address} and ${output.password}…
- runScript:
file: proxymail.js
env:
ACTION: return

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, transmettez l’exécution qu’elle lance comme la clé, et ajoutez TEST au env de chaque script pour nommer l’étape :

maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN signup.yaml

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