rubis/e2e/tests/import.spec.ts
ordinarthur 0ecf8ad40f test(e2e): PR 2 — import OCR + plans + mailing Mailpit
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>
2026-05-18 16:45:39 +02:00

95 lines
3.3 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { join } from 'node:path'
import { resetDb } from './helpers/api'
/**
* Scénarios import OCR via /factures/import.
*
* En mode `test_e2e`, le provider OCR est `mock` (cf. .env de l'API
* spawn par Playwright config), donc l'upload d'un PDF déclenche le
* MockOcrProvider qui génère des champs plausibles depuis le filename
* sans appel réseau.
*
* On utilise un vrai PDF du dossier fixtures (qui est ignoré par git
* mais existe en local) pour tester l'upload multipart réel. Si le
* dossier est vide (CI fresh), les tests sont skip.
*/
const FIXTURE_PDF = join(
process.cwd(),
'e2e',
'fixtures',
'invoices',
'facture-pas-en-retard-echeance-30j-001.pdf',
)
async function signupAndOnboard(page: import('@playwright/test').Page, tag = 'imp') {
const email = `${tag}+${Date.now()}@rubis.test`
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('Test User')
await page.getByLabel(/Email professionnel/i).fill(email)
await page.getByLabel(/Mot de passe/i).fill('motdepasse-fort-123')
await page.getByRole('button', { name: /créer mon compte/i }).click()
await page.waitForURL(/\/onboarding\/compte/, { timeout: 10_000 })
await page.getByRole('button', { name: /continuer/i }).click()
await page.waitForURL(/\/onboarding\/entreprise/)
await page.getByLabel(/nom de l'entreprise/i).fill('Atelier Test')
await page.getByRole('button', { name: /continuer/i }).click()
await page.waitForURL(/\/onboarding\/signature/)
await page.getByRole('button', { name: /terminer/i }).click()
await page.waitForURL('/', { timeout: 10_000 })
}
test.describe('Import factures via dropzone', () => {
test.beforeEach(async () => {
await resetDb()
})
test('upload PDF → drafts générés + redirection /import/$batchId', async ({
page,
}) => {
// Skip si pas de fixture (CI fresh)
try {
// existsSync via Playwright fs
const { existsSync } = await import('node:fs')
if (!existsSync(FIXTURE_PDF)) {
test.skip(true, `Pas de PDF fixture (${FIXTURE_PDF}) — skip`)
}
} catch {
test.skip(true, 'fs indispo')
}
await signupAndOnboard(page, 'imp-upload')
await page.goto('/factures/import')
await expect(
page.getByRole('heading', { name: /importer.*plusieurs.*factures/i }),
).toBeVisible()
// Le file input est caché derrière le Dropzone — Playwright peut
// setInputFiles directement même sur un input hidden.
const fileInput = page.locator('input[type="file"]')
await fileInput.setInputFiles(FIXTURE_PDF)
// Le SPA fait un POST multipart à /api/v1/invoices/upload puis
// navigate sur /factures/import/$batchId
await page.waitForURL(/\/factures\/import\/[a-f0-9-]+/, { timeout: 15_000 })
// Au moins 1 draft visible (le filename apparaît quelque part)
await expect(
page.getByText(/facture-pas-en-retard/).first(),
).toBeVisible({ timeout: 5_000 })
})
test('page /factures/import affiche le dropzone vide', async ({ page }) => {
await signupAndOnboard(page, 'imp-empty')
await page.goto('/factures/import')
// Texte du dropzone
await expect(
page.getByText(/PDF.*PNG.*JPG.*fichiers.*simultané/i),
).toBeVisible()
// Bouton parcourir
await expect(page.getByRole('button', { name: /parcourir.*fichiers/i })).toBeVisible()
})
})