Ajoute 6 scénarios Playwright qui couvrent les surfaces utilisateur
au-delà de l'auth/clients/factures : upload OCR via dropzone, listing
des plans pré-fournis, et la chaîne mailing complète end-to-end.
Import (2 tests) — import.spec.ts :
- Upload PDF via setInputFiles : MockOcrProvider extrait depuis le
filename → drafts créés → SPA redirige sur /factures/import/$batchId
→ on voit le filename listé dans les drafts
- Empty state /factures/import : dropzone + bouton "Parcourir" visibles
Plans (3 tests) — plans.spec.ts :
- Les 4 plans pré-fournis (Standard, Rapide, Patient, Ferme) sont
visibles dès le signup (provisionnés par provisionDefaultPlans)
- Clic "Créer un plan" → arrive sur le wizard /plans/nouveau
- Clic "Modifier" sur une PlanCard → page détail /plans/{slug}
Mailing (1 test, le plus précieux) — mailing.spec.ts :
- Helper helpers/mailpit.ts : clearMailpit, waitForMessageTo (poll
200ms / 10s timeout), getMessage (HTML + texte)
- Scénario : signup → onboarding → créer client avec email →
créer facture saisie manuelle → mark-paid → BullMQ worker enqueue
payment-thanks → email envoyé via SMTP → Mailpit catche →
test inspecte subject + body (numero, montant)
- Valide la chaîne complète : SPA → API → DB → BullMQ → Worker →
Mailpit, en moins de 5 s
Total après cette PR : 26 scénarios Playwright verts en 55 s.
Pré-requis runtime :
- `pnpm dev:up` (Postgres + Redis + Mailpit + MinIO)
- `pnpm e2e:setup` (création DB rubis_test_e2e + migrations)
- Mailpit accessible sur :8025 (clearMailpit l'utilise en beforeEach)
Note check-in flow : pas implémenté en E2E V1 — nécessite un endpoint
test_e2e pour générer un CheckinTask + clear token. Le flow checkin
est déjà couvert par les tests japa functional. À ajouter en PR 3 si
besoin de validation end-to-end UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { request, type APIRequestContext } from '@playwright/test'
|
|
|
|
/**
|
|
* Helpers pour interagir avec Mailpit (SMTP catcher local).
|
|
*
|
|
* Mailpit tourne sur http://localhost:8025 via docker-compose dev
|
|
* (cf. /docker-compose.dev.yml). L'API REST permet de :
|
|
* - Lister les messages reçus
|
|
* - Récupérer le contenu HTML/texte d'un message
|
|
* - Vider la boîte (utile en beforeEach E2E)
|
|
*
|
|
* Doc API : https://mailpit.axllent.org/docs/api-v1/
|
|
*/
|
|
|
|
const MAILPIT_URL = process.env.E2E_MAILPIT_URL ?? 'http://localhost:8025'
|
|
|
|
let _ctx: APIRequestContext | null = null
|
|
async function ctx(): Promise<APIRequestContext> {
|
|
if (_ctx) return _ctx
|
|
_ctx = await request.newContext({ baseURL: MAILPIT_URL })
|
|
return _ctx
|
|
}
|
|
|
|
export type MailpitMessage = {
|
|
ID: string
|
|
MessageID: string
|
|
Read: boolean
|
|
From: { Name: string; Address: string }
|
|
To: Array<{ Name: string; Address: string }>
|
|
Subject: string
|
|
Created: string
|
|
Snippet: string
|
|
}
|
|
|
|
export type MailpitMessageDetail = MailpitMessage & {
|
|
Text: string
|
|
HTML: string
|
|
ReplyTo: Array<{ Name: string; Address: string }>
|
|
}
|
|
|
|
/**
|
|
* Vide TOUS les messages Mailpit. À appeler en `beforeEach` E2E pour
|
|
* isoler les assertions email entre scénarios.
|
|
*/
|
|
export async function clearMailpit(): Promise<void> {
|
|
const c = await ctx()
|
|
await c.delete('/api/v1/messages')
|
|
}
|
|
|
|
/**
|
|
* Liste les messages reçus, plus récent en premier.
|
|
*/
|
|
export async function listMessages(): Promise<MailpitMessage[]> {
|
|
const c = await ctx()
|
|
const r = await c.get('/api/v1/messages')
|
|
if (!r.ok()) throw new Error(`Mailpit list failed: ${r.status()}`)
|
|
const json = (await r.json()) as { messages: MailpitMessage[] }
|
|
return json.messages ?? []
|
|
}
|
|
|
|
/**
|
|
* Attend qu'au moins un message arrive pour l'adresse `to`, avec polling
|
|
* toutes les 200 ms (timeout 10 s par défaut). Renvoie le 1er match.
|
|
*
|
|
* Utile parce que les emails partent via BullMQ worker (async) — l'instant
|
|
* où on clique sur "Relancer maintenant" et l'instant où Mailpit reçoit
|
|
* peuvent être espacés de 1-2 s.
|
|
*/
|
|
export async function waitForMessageTo(
|
|
to: string,
|
|
opts: { timeoutMs?: number; subjectIncludes?: string } = {},
|
|
): Promise<MailpitMessage> {
|
|
const timeoutMs = opts.timeoutMs ?? 10_000
|
|
const deadline = Date.now() + timeoutMs
|
|
while (Date.now() < deadline) {
|
|
const messages = await listMessages()
|
|
const match = messages.find(
|
|
(m) =>
|
|
m.To.some((t) => t.Address.toLowerCase() === to.toLowerCase()) &&
|
|
(!opts.subjectIncludes || m.Subject.includes(opts.subjectIncludes)),
|
|
)
|
|
if (match) return match
|
|
await new Promise((r) => setTimeout(r, 200))
|
|
}
|
|
throw new Error(
|
|
`Mailpit : aucun message reçu pour ${to}${opts.subjectIncludes ? ` (subject contient "${opts.subjectIncludes}")` : ''} dans ${timeoutMs} ms`,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Récupère le détail d'un message (HTML + texte).
|
|
*/
|
|
export async function getMessage(id: string): Promise<MailpitMessageDetail> {
|
|
const c = await ctx()
|
|
const r = await c.get(`/api/v1/message/${id}`)
|
|
if (!r.ok()) throw new Error(`Mailpit get failed: ${r.status()}`)
|
|
return (await r.json()) as MailpitMessageDetail
|
|
}
|