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 }) }) test('crée une facture avec un client inconnu (à la volée) — fournit l\'email', async ({ page, }) => { // Cas user réel : pas envie de quitter le dialog de création de facture // pour aller créer le client d'abord. Le combobox propose "Créer le // client X" → on clique, on remplit l'email du client juste après, on // soumet la facture. Le client est créé en DB en même temps que la // facture, en un seul flow. // // Avant le fix UX (2026-05-18) : le dialog ne demandait PAS l'email // du client → POST /invoices renvoyait 422 client_email_required → // toast générique "Création impossible. Vérifiez les champs." → l'user // ne pouvait pas comprendre ni s'en sortir sans fermer le dialog. Ce // test l'a démontré et a guidé le fix. await signupAndOnboard(page, 'fac-newclient') // Pas de client préexistant dans la DB. await page.getByRole('button', { name: /^saisir$/i }).first().click() await expect(page.getByRole('dialog')).toBeVisible() // Taper un nom de client inconnu → la dropdown propose "Créer le client". const combobox = page.getByPlaceholder(/rechercher.*créer un client/i) await combobox.fill('Café du Coin Test') // L'option "Créer le client « Café du Coin Test »" apparaît. const createOption = page.getByRole('button', { name: /créer le client.*café du coin test/i, }) await expect(createOption).toBeVisible({ timeout: 3_000 }) await createOption.click() // Après le fix, un champ "Email du client" devient visible dans le // dialog (conditionnel sur `clientId === null && clientName non vide`). const emailField = page.getByLabel(/email du client/i) await expect(emailField).toBeVisible({ timeout: 3_000 }) await emailField.fill('contact@cafeducoin.test') // Reste du form await page.getByLabel(/N° de facture/i).fill('F-NEWCLI-001') await page.getByLabel(/Montant TTC/i).fill('850') await page.getByLabel(/Plan de relance/i).click() await page.getByRole('option').first().click() await page.getByRole('button', { name: /^créer la facture$/i }).click() // Toast success + dialog fermé await expect(page.getByText(/facture créée/i)).toBeVisible({ timeout: 5_000 }) // La facture est visible dans /factures avec le nouveau client await page.goto('/factures') await expect(page.getByText('F-NEWCLI-001').first()).toBeVisible({ timeout: 5_000, }) await expect(page.getByText(/café du coin test/i).first()).toBeVisible() // Le client a aussi été créé en DB → visible sur /clients await page.goto('/clients') await expect(page.getByText(/café du coin test/i).first()).toBeVisible() await expect(page.getByText('contact@cafeducoin.test').first()).toBeVisible() }) })