Zum Inhalt springen

proxymail-qa kapselt die REST API für JavaScript und TypeScript: Playwright, Cypress-Tasks, Vitest, Jest, Detox oder ein einfaches Skript. Es hat keine Abhängigkeiten, bringt eigene Typen mit und läuft auf Node 18 oder neuer (oder überall mit fetch). Der Quellcode liegt unter github.com/aaquib90/proxymail-qa-js, und jedes Release wird aus GitHub Actions mit npm-Provenance veröffentlicht.

  1. npm install --save-dev proxymail-qa
  2. Erstelle einen API-Schlüssel und setze ihn als PROXYMAIL_QA_API_KEY, oder übergib ihn direkt.

  3. Erstelle eine Adresse, registriere dich damit in deiner App und warte dann auf den Code oder Link.

    Wartevorgänge geben nur den Code oder Link zurück, mit dem Dienst, der ihn gesendet hat, und seinem Ablaufzeitpunkt, nie den Text der E-Mail. Ein Wartevorgang hält bis zu 30 Sekunden, längere timeouts wiederholen ihn.

    import { ProxyMailQA } from 'proxymail-qa';
    const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEY
    const { address, persona, password } = await qa.createAddress({ persona: true, password: true });
    const submitted = new Date();
    // …sign up in your app with address, persona and password…
    const { value: code } = await qa.waitForCode(address, { since: submitted });
    // or: const { value: link } = await qa.waitForLink(address, { type: 'verify', since: submitted });
    await qa.deleteAddress(address);

Vollständige Beispiele: Playwright und Cypress.

Methode Was es tut
new ProxyMailQA({ apiKey?, baseUrl?, fetch?, run? }) Der Client. apiKey ist standardmäßig PROXYMAIL_QA_API_KEY und run standardmäßig PROXYMAIL_QA_RUN
createAddress(options?) Eine neue Testadresse. Optionen: label, name (ein eigener Name vor dem @; generische Namen wie test allein werden abgelehnt), persona, password (einmal zurückgegeben und als string typisiert, wenn du password: true übergibst), lifetimeDays, domain, age oder dateOfBirth, country und wo sie verwendet wird: site und environment (neu in 0.9.0)
waitForCode(address, options?) Der neueste Verifizierungs- oder Login-Code. Optionen: since, service, timeout (Sekunden, Standard 30), unused (bereits verwendete Codes überspringen: von einem früheren Wartevorgang zurückgegeben oder in der App oder Proxy Mail QA for Chrome kopiert oder ausgefüllt; neu in 0.5.1) und type: 'coupon' für einen Gutschein- oder Promo-Code, mit discount, valid_until und after_seconds (neu in 0.10.0)
waitForLink(address, options?) Dasselbe für einen Link. Außerdem type (verify, magic_signin, invite, reset oder any, oder eine Liste) und app (true für den Link, der eine mobile App öffnet)
messages(address, options?) E-Mails mit ihren Codes und Links. includeText und includeHtml fügen den Inhalt hinzu, für Template-Tests; limit
countEmails(address, options?) Wie viele E-Mails angekommen sind, jede ohne Inhalt zusammengefasst. Optionen: since, service, type (code oder link), subtype und ab 0.10.0 category (auth, transactional oder marketing)
expectNoEmail(address, options?) Wird erfüllt, wenn innerhalb von timeout Sekunden keine passende E-Mail ankommt (Standard 30, auch Minuten funktionieren: es fragt erneut); wirft ProxyMailQAError (409, email_arrived), sobald eine ankommt. Dieselben Filter, und since ist standardmäßig jetzt
markUnsubscribed(address, options?) Labs. Erfasst, dass sich die Adresse bei einem Absender abgemeldet hat (emailId oder service; at zum Rückdatieren; grace: '1d' für bereits unterwegs befindliche Mails), nachdem der Test das im Browser erledigt hat. Jede spätere Marketing-E-Mail dieses Absenders lässt after_unsubscribe fehlschlagen. Neu in 0.10.0
markResubscribed(address, options?) Labs. Die Adresse hat erneut abonniert: beendet die Überwachung. Neu in 0.10.0
recordConsent(address, consent) Labs. Erfasst, wozu das Konto bei der Registrierung zugestimmt hat (marketing, prechecked, doubleOptIn), für die Marketingregeln je Land. consent(address) liest es. Neu in 0.10.0
consent(address) Labs. Wozu das Konto laut Erfassung zugestimmt hat. Neu in 0.10.0
recordCouponResult(address, result, options?) Labs. Erfasst nach dem Anwenden eines Gutscheins beim Checkout (Staging, Testzahlungen) accepted, rejected oder error mit message der App (code, oneTime); check ist das coupon_result-Ergebnis. Neu in 0.10.0
checkSequence(address, steps, options?) Labs. Ob eine Willkommensserie oder Drip-Kampagne nach since in der richtigen Reihenfolge und pünktlich ankam; jeder Schritt kann subject, category, type, after (‘1d’) und within angeben. Neu in 0.10.0
offers(address) Labs. Die Gutscheine jedes Absenders und an welche Variante der Adresse (normal, Großbuchstaben, +tag) jeder ging; passed ist false, wenn eine Person ein Angebot zweimal bekam. Neu in 0.10.0
checkUnsubscribe(address) Labs. Jede Abmeldung und die Marketing-E-Mails, die danach kamen; passed ist false, wenn welche kamen. Neu in 0.10.0
forTest(name), recordTest(result), recordTestTotals(totals) Ein Client, der in jedem Aufruf seinen Test benennt, und Testergebnisse für den Bericht des Laufs; proxymail-qa/playwright und sein Reporter nutzen sie. Neu in 0.8.0 (mehr)
formValues({ fields, address?, age?, country?, … }) Was in jedes Feld eines Registrierungsformulars einzugeben oder auszuwählen ist, aus einer dafür erstellten neuen Adresse. Neu in 0.7.0 (mehr)
checkEmail(address, options?) Prüft die neueste passende E-Mail (oder emailId): SPF, DKIM, DMARC, lokale und einfache http-Links, wohin ihr Link führt (expectHost), ein angegebenes Ablaufdatum (und ab 0.10.0, wann ein Gutschein endet), einen Klartextteil, Größe und reine Bild-E-Mails. Ab 0.7.0 Formulierungen, die sie enthalten soll (expectText), und ihre Sprache (expectLanguage); ab 0.10.0, wie schnell nach since sie kam (expectWithin: '5m'), und ihre Anhänge (expectAttachment: ['.pdf']). Eine Marketing-E-Mail bekommt außerdem list_unsubscribe, one_click_unsubscribe, unsubscribe_link und preheader, und jede E-Mail harmful_attachment und spam_risk (eine Schätzung, ob sie im Spam landet). Nur Ergebnisse, nie ihr Inhalt. Neu in 0.4.0
checkDelivery(address) Was die Adresse erreicht hat und was abgewiesen wurde, ohne zu warten
variants(address) Ungewöhnliche Varianten der Adresse, die trotzdem ihre Mails bekommen, und ungültige (mehr)
nameVariants({ field?, maxLength? }) Knifflige Namen für ein Namensfeld: welche zu akzeptieren sind (Apostrophe, Akzente, andere Schriften), welche abzulehnen sind, HTML oder SQL, das als Text zu speichern ist, und welche Apps unterschiedlich behandeln. Neu in 0.11.0 (mehr)
passwordVariants({ minLength?, maxLength?, symbols? }) Knifflige Passwörter innerhalb der Feldregeln: Minimum und Maximum, jede Symbolklasse, Leerzeichen, Nicht-ASCII und welche Apps unterschiedlich behandeln; bei jedem Aufruf neu. Neu in 0.11.0 (mehr)
getAddress(address), listAddresses({ limit?, before?, tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment? }) Adressen lesen oder früher erstellte finden: Jeder angegebene Filter muss passen
updateAddress(address, patch) label, notes, tags, first_name, last_name, username, date_of_birth oder country ändern; null löscht einen Wert
setPassword(address, { password?, label? }) Speichere das Passwort, das das Testkonto jetzt verwendet, nachdem die App es geändert oder zurückgesetzt hat. Ohne Angabe wird ein starkes Passwort erzeugt und einmal zurückgegeben
savedPassword(address, label?) Ein gespeichertes Passwort lesen. Braucht einen Schlüssel mit „Kann gespeicherte Passwörter lesen“; jeder Lesezugriff wird protokolliert
signInAccount({ site?, environment?, tags? }) Das gespeicherte Testkonto zum Anmelden und sein Passwort in einem Aufruf: zuerst das, das auf dieser Website verwendet wurde; null, wenn keines passt. Braucht einen Schlüssel mit „Darf gespeicherte Passwörter lesen“. Neu in 0.9.0 (mehr)
markDevHost(host) Markiert einen Host wie localhost:3000 als Entwicklungsumgebung des Projekts, sodass Links dorthin checkEmail bestehen. Neu in 0.9.0
deleteAddress(address) Adresse und ihre E-Mails löschen
project() Das Projekt des Schlüssels: seine Mail-Domains, sein E-Mail-Präfix und seine Limits
updateProject({ name?, website?, environments?, emailPrefix?, mailDomain? }) Ändert das Projekt wie in den Projekteinstellungen; null leert website oder emailPrefix. Braucht die Projektleitung oder einen Owner oder Admin der Organisation
checkOutAccount({ tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment?, minutes? }), extendCheckout(account, minutes?), returnAccount(account, { addTags?, removeTags? }), withAccount(options, fn) Der Konten-Pool: Leih dir ein Konto in einem bekannten Zustand, das kein anderer Test hat, und gib es zurück. Neu in 0.6.0 (mehr). Ab 0.7.0 nimmt withAccount die Hooks create, check und reset: Konten in einem bekannten Zustand
startRun({ name?, source?, meta? }), finishRun({ failed? }), runReport(), addRunNote(text, { failed? }), getRun(), listRuns() Ein Run: Die Aufrufe dieses Clients fügen hinzu, was sie gesehen haben, für einen Bericht im Pull Request. Neu in 0.5.0

Jede Methode nimmt eine einfache Adresse oder eine Variante davon. Das Warten auf eine +tag-Adresse findet nur E-Mails an diesen Tag, sodass eine Adresse für einen ganzen Testlauf reicht: siehe In CI ausführen.

Mit Labs markierte Methoden gehören zu Marketing- und Angebots-E-Mails, einer Labs-Funktion, die der Ersteller des Schlüssels in der App unter Account settings → Labs einschaltet; bis dahin werfen sie einen 403-Fehler, der das sagt. checkEmail lässt die Prüfungen von Labs-Funktionen weg, die aus sind.

formValues liefert, was du in jedes Feld eines Formulars eingeben oder auswählen sollst, aus einer dafür neu erstellten Adresse: Benutzername und Passwort, die den Regeln der Felder entsprechen, das Geburtsdatum im Format des Felds, ein Land, das die Auswahlliste anbietet. Lies in Playwright die Felder von der Seite und fülle jedes aus:

const form = page.locator('form#sign-up');
const fields = await form.locator('input, select').evaluateAll((elements) =>
elements.map((el, i) => {
const input = el as HTMLInputElement & HTMLSelectElement;
input.dataset.field = String(i);
return {
id: String(i),
type: input.tagName === 'SELECT' ? 'select' : input.type,
name: input.name,
label: input.labels?.[0]?.textContent ?? input.getAttribute('aria-label') ?? '',
placeholder: input.placeholder,
autocomplete: input.getAttribute('autocomplete') ?? '',
pattern: input.pattern || undefined,
minLength: input.minLength > 0 ? input.minLength : undefined,
maxLength: input.maxLength > 0 ? input.maxLength : undefined,
options: input.tagName === 'SELECT' ? Array.from(input.options, (o) => ({ value: o.value, label: o.text })) : undefined,
};
}),
);
const { address, values } = await qa.formValues({ fields });
for (const { id, kind, value } of values) {
if (value === null) continue;
const field = form.locator(`[data-field="${id}"]`);
if (await field.evaluate((el) => el.tagName === 'SELECT')) await field.selectOption(value);
else await field.fill(value);
}

Übergib address, um ein früher erstelltes Konto zu verwenden, und age oder country für ein neues. Mehr im Leitfaden REST API.

Prüfen, dass nichts ankommt oder genau eine E-Mail

Abschnitt betitelt „Prüfen, dass nichts ankommt oder genau eine E-Mail“
// An unsubscribed user gets no newsletter.
const sent = new Date();
await sendNewsletter();
await qa.expectNoEmail(unsubscribed.address, { since: sent, timeout: 20 });
// A sign-up sends one verification email, not two.
await qa.waitForLink(address, { since: submitted });
await new Promise((resolve) => setTimeout(resolve, 10_000)); // time for a duplicate
expect((await qa.countEmails(address, { since: submitted, type: 'link' })).count).toBe(1);

Keine der beiden gibt zurück, was in den E-Mails steht, nur wann jede ankam, wer sie gesendet hat und welche Art Code oder Link sie hatte. Mehr im Leitfaden REST API.

await qa.waitForLink(address, { since: submitted });
const result = await qa.checkEmail(address, { since: submitted, type: 'link', expectHost: 'staging.example.com' });
expect(result.failed).toEqual([]); // e.g. ['local_links'] when the link goes to localhost
// A localised email: the right language, with the user's name in it.
const german = await qa.checkEmail(address, { since: submitted, expectLanguage: 'de', expectText: [persona.first_name] });
expect(german.failed).toEqual([]);

Jede Prüfung besteht, schlägt fehl, warnt oder wird übersprungen, mit einer einzeiligen Begründung; was jede bedeutet, steht im Leitfaden REST API. Testest du eine App, die auf deinem Rechner läuft? await qa.markDevHost('localhost:3000') einmal, und Links dorthin bestehen local_links; Links zu jedem anderen localhost schlagen weiterhin fehl.

Tagge Konten mit ihrem Zustand und lass dann jeden Test eines ausleihen. withAccount checkt eines aus, führt deine Funktion aus und gibt es immer zurück; gib { addTags, removeTags } zurück, um festzuhalten, was der Test geändert hat:

await qa.withAccount({ tags: ['verified', 'premium'] }, async (account) => {
const { secret } = await qa.savedPassword(account.address);
await signIn(page, account.address, secret);
await cancelSubscription(page);
return { addTags: ['cancelled'], removeTags: ['premium'] };
});

Kein anderer Test bekommt dasselbe Konto, solange es ausgecheckt ist (10 Minuten, wenn du nicht minutes übergibst), und eines, das nie zurückgegeben wird, gibt sich selbst frei, wenn seine Zeit abläuft.

Braucht ein Test ein Konto in einem bestimmten Zustand in deiner App (verifiziert, im Premium-Tarif, ohne Bestellungen), gib withAccount drei Hooks. Dein Testcode führt sie mit seinem eigenen Zugriff auf deine App aus; Proxy Mail QA meldet sich nie dort an. Hier steht app für deinen eigenen Code, der mit deiner App spricht: eine API für Admins, ein Datenbank-Seed oder die UI.

await qa.withAccount(
{
tags: ['verified', 'premium'],
// When none is free: sign up with the new address, verify it, upgrade it.
create: async (account) => {
const submitted = new Date();
await app.signUp(account.address, account.password);
const { value: link } = await qa.waitForLink(account.address, { type: 'verify', since: submitted });
await app.open(link);
await app.admin.setPlan(account.address, 'premium');
},
// Is it still in that state? Return false or throw if not.
check: async (account) => (await app.admin.user(account.address)).plan === 'premium',
// After the test: put it back.
reset: async (account) => app.admin.resetUser(account.address, { plan: 'premium', orders: [] }),
},
async (account) => {
// …the test…
},
);
  • check läuft, bevor der Test das Konto bekommt. Schlägt es fehl, läuft reset und es wird erneut geprüft; schlägt es weiter fehl, wird das nächste Konto versucht (bis zu attempts, 3), und das fehlgeschlagene verliert die Tags des Pools und bekommt den Tag check-failed.
  • create läuft, wenn kein Konto passt oder jeder Treffer ausgecheckt ist. Die neue Adresse ist von Anfang an für diesen Test reserviert und bekommt die Tags, sobald create und check erfolgreich sind. Übergib newAccount für Alter, Label oder Domain; sie bekommt das Land des Filters.
  • reset läuft nach dem Test, egal ob er bestanden hat oder nicht. Schlägt reset fehl, verliert das Konto die Tags des Pools und bekommt den Tag needs-reset, sodass kein anderer Test es in einem unbekannten Zustand bekommt.

In einem Run fügen ein neues Konto, ein fehlgeschlagener check und ein fehlgeschlagener reset jeweils eine Zeile zum Bericht hinzu.

Aufrufe, die bei offenem Run gemacht werden, fügen ihm hinzu, was sie gesehen haben: die Adressen, welche E-Mails ankamen und wie lange nach since, die Codes, wohin jeder Link führt, Prüfungen, Abmelde-, Angebots- und Gutscheinprüfungen, fehlgeschlagene Tests und E-Mails, die nie kamen. In GitHub Actions startet und beendet die Proxy Mail QA-Action den Run und postet den Bericht im Pull Request; der Client übernimmt den Run aus PROXYMAIL_QA_RUN, ohne dass du deine Tests ändern musst. Anderswo führst du ihn selbst aus:

const run = await qa.startRun({ name: 'Sign-up tests', meta: { branch: process.env.BRANCH } });
// …the tests, using qa…
await qa.finishRun({ failed: testsFailed });
console.log(await qa.runReport(run.id)); // Markdown, for a pull request comment

Andere Prozesse treten dem Run bei, wenn du ihnen seine ID als PROXYMAIL_QA_RUN übergibst.

Um zu zeigen, aus welchem Test jede E-Mail kam, verwende pro Test einen Client: qa.forTest('sign-up › sends one code') nennt den Test in jedem Aufruf. In Playwright gibt proxymail-qa/playwright jedem Test eine qa-Fixture, die das tut, und proxymail-qa/playwright/reporter hält fehlgeschlagene und instabile Tests sowie die Summen im Run fest: siehe In CI ausführen. Andere Test-Runner können Ergebnisse selbst mit qa.recordTest({ name, status, durationMs, error }) und qa.recordTestTotals({ passed, failed, skipped, flaky }) senden. Neu in 0.8.0.

Fehlgeschlagene Aufrufe werfen ProxyMailQAError mit dem HTTP-status, einem code und einer lesbaren Meldung. Findet ein Warten nichts, sagt die Meldung auch, warum, soweit Proxy Mail QA das erkennen kann (error.hint), zum Beispiel dass der E-Mail-Anbieter der App nur an verifizierte Adressen sendet, und error.refused listet E-Mails auf, die abgewiesen wurden, weil sie zu groß waren, über einem Limit lagen oder von einer Website kamen, mit der Proxy Mail QA nicht funktioniert (refused[].site nennt sie):

import { ProxyMailQAError } from 'proxymail-qa';
try {
await qa.waitForCode(address, { since: submitted });
} catch (error) {
if (error instanceof ProxyMailQAError && error.status === 404) console.log(error.hint);
throw error;
}

Ein neuer Versuch hilft erst, wenn die App an jede Adresse senden kann. Siehe Test-E-Mail kommt nicht an?

Tests und Agents können zu einem früher erstellten Konto zurückkehren, statt ein neues anzulegen. Jeder angegebene Filter muss passen; das Alter wird in ganzen Jahren ab heute aus dem Geburtsdatum der Persona berechnet, und search sucht in Adresse, Label, Notizen, Name und Benutzername.

// An under-18 in the UK, made for the age-gate tests, with a saved password.
const [teen] = await qa.listAddresses({ tags: ['age-gate'], country: 'GB', maxAge: 17, hasPassword: true });
// Record where an account is used, then sign in with it again later: the
// account used on this site first, with its password (needs a key with
// "Can read saved passwords").
await qa.createAddress({ password: true, site: 'staging.example.com', environment: 'staging' });
const account = await qa.signInAccount({ site: 'staging.example.com', tags: ['verified'] });

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.

Doku fragen