rubis/e2e/tests/factures.spec.ts
ordinarthur 70c851dd0e test(e2e): PR 1 — auth complet + clients CRUD + factures saisie
Étend la suite Playwright avec 14 nouveaux scénarios couvrant les
surfaces critiques que tout user touche au quotidien.

Auth (7 tests) — auth.spec.ts :
  - Signup : email invalide (HTML5), email déjà pris (422 → toast)
  - Login : happy path après signup, mauvais password (401), email
    inconnu (401)
  - Protection des routes : / et /factures redirigent vers /login
    sans session

Clients (5 tests) — clients.spec.ts :
  - Create via dialog : remplir Nom + Email du contact → apparaît
    dans la liste, compteur "1 fiche"
  - Refuse email manquant (422, dialog reste ouvert)
  - 2 clients distincts → compteur "2 fiches"
  - Duplicate par nom case-insensitive → liste reste à 1 fiche
  - Recherche ILIKE par nom → filtre côté liste

Factures (2 tests) — factures.spec.ts :
  - Saisie manuelle complète : créer client puis facture via le
    dialog (combobox client async + numéro + montant + Radix Select
    plan) → apparaît dans /factures
  - Empty state visible si aucune facture

Total Playwright après cette PR : 20 scénarios verts en 38 s.

Stratégie : les edge cases déjà couverts par les couches inférieures
(unit, functional japa, vitest) ne sont PAS re-testés en E2E pour
éviter la duplication. Le E2E garde son rôle : happy path UI + edge
cases produit qui n'apparaissent qu'au niveau navigation/forms.

Prochaines PRs prévues :
  - PR 2 : OCR upload + Plans + Relances + Mailpit (mailing)
  - PR 3 : Billing complet (trial→active/past_due/cancel) + Dashboard
    KPIs + Settings
  - PR 4 : Blog + edge cases globaux + coverage report c8

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:07:55 +02:00

102 lines
4.1 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { resetDb } from './helpers/api'
/**
* Scénarios facture — saisie manuelle via dialog.
*
* Le dialog combine combobox client async, DatePicker custom, Radix Select
* (échéance + plan), Input montant. On vise le happy path UI ;
* les edge cases (numéro unique, quota Free 2, cross-org, payload
* incomplet) sont déjà couverts par les tests japa functional/unit.
*/
async function signupAndOnboard(page: import('@playwright/test').Page, tag = 'fac') {
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 })
return { email }
}
async function createClientViaUI(
page: import('@playwright/test').Page,
name: string,
email: string,
) {
await page.goto('/clients')
await page.getByRole('button', { name: /nouveau client/i }).first().click()
await page.getByLabel(/^Nom du client$/i).fill(name)
await page.getByLabel(/^Email du contact$/i).fill(email)
await page.getByRole('button', { name: /^créer le client$/i }).click()
await expect(page.getByText(name).first()).toBeVisible({ timeout: 5_000 })
}
test.describe('Factures — saisie manuelle via dialog', () => {
test.beforeEach(async () => {
await resetDb()
})
test('crée une facture liée à un client existant + apparaît dans /factures', async ({
page,
}) => {
await signupAndOnboard(page, 'fac-manual')
// 1. Prérequis : un client en DB
await createClientViaUI(page, 'Boulangerie Test', 'compta@boulangerie.test')
// 2. Ouvrir le dialog "+ Saisir" depuis le header desktop
await page.getByRole('button', { name: /^saisir$/i }).first().click()
await expect(page.getByRole('dialog')).toBeVisible()
// 3. Combobox client — taper le nom puis sélectionner l'option dans
// la dropdown (recherche async côté API).
const combobox = page.getByPlaceholder(/rechercher.*créer un client/i)
await combobox.fill('Boulangerie')
// Attendre que l'option remontée par l'API apparaisse
await expect(page.getByRole('option', { name: /Boulangerie Test/i })).toBeVisible({
timeout: 5_000,
})
await page.getByRole('option', { name: /Boulangerie Test/i }).click()
// 4. Numéro de facture
await page.getByLabel(/N° de facture/i).fill('F-E2E-001')
// 5. Montant TTC (en euros, le form convertit en cents)
await page.getByLabel(/Montant TTC/i).fill('1240')
// 6. Plan (Radix Select) — on prend le 1er disponible
await page.getByLabel(/Plan de relance/i).click()
// L'item Radix s'affiche via Portal — on cible par rôle option
await page.getByRole('option').first().click()
// 7. Submit
await page.getByRole('button', { name: /^créer la facture$/i }).click()
// 8. Le dialog se ferme + redirection vers /factures (route handler du form)
// ou bien rester sur la page courante. On navigue manuellement pour
// vérifier la persistence.
await page.goto('/factures')
await expect(page.getByText('F-E2E-001').first()).toBeVisible({ timeout: 10_000 })
await expect(page.getByText('Boulangerie Test').first()).toBeVisible()
})
test('liste vide → empty state visible', async ({ page }) => {
await signupAndOnboard(page, 'fac-empty')
await page.goto('/factures')
// Empty state avec un texte parlant
await expect(
page.getByText(/aucune facture|pas encore.*facture/i).first(),
).toBeVisible({ timeout: 5_000 })
})
})