import { test, expect } from '@playwright/test' import { resetDb } from './helpers/api' /** * Scénarios CRUD clients : * - Création via dialog (happy path) * - Validation : email obligatoire * - Recherche (filtre par nom) * - Duplicate (409) → message clair, le client existant reste affiché * - Empty state quand pas de clients */ async function signupAndOnboard(page: import('@playwright/test').Page, tag = 'cli') { 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/) 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 } } test.describe('Clients — CRUD via dialog', () => { test.beforeEach(async () => { await resetDb() }) test('crée un client via le dialog + apparaît dans la liste', async ({ page, }) => { await signupAndOnboard(page, 'cli-create') await page.goto('/clients') await expect( page.getByRole('heading', { name: /^clients/i }), ).toBeVisible() // Empty state initial await expect(page.getByText(/pas encore de.*clients/i)).toBeVisible() // Ouvrir le dialog await page.getByRole('button', { name: /nouveau client/i }).first().click() // Remplir le form await page.getByLabel(/^Nom du client$/i).fill('Boulangerie Martin') await page.getByLabel(/^Email du contact$/i).fill('contact@martin.fr') await page.getByRole('button', { name: /^créer le client$/i }).click() // Le dialog se ferme + le client apparaît dans la liste await expect(page.getByText('Boulangerie Martin').first()).toBeVisible({ timeout: 5_000, }) await expect(page.getByText('contact@martin.fr').first()).toBeVisible() // Le compteur passe à "1 fiche" await expect(page.getByText(/·\s*1\s*fiche/i)).toBeVisible() }) test('refuse un email manquant (422)', async ({ page }) => { await signupAndOnboard(page, 'cli-noemail') await page.goto('/clients') await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill('Sans Email') // Email vide → on soumet await page.getByRole('button', { name: /^créer le client$/i }).click() // Le dialog reste ouvert + un message d'erreur près du champ email // (le HTML5 required ou la validation Zod côté form) await expect(page.getByRole('button', { name: /^créer le client$/i })).toBeVisible() }) test('crée 2 clients distincts + affichage du compteur', async ({ page }) => { await signupAndOnboard(page, 'cli-two') await page.goto('/clients') // Client 1 await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill('Atelier A') await page.getByLabel(/^Email du contact$/i).fill('a@example.com') await page.getByRole('button', { name: /^créer le client$/i }).click() await expect(page.getByText('Atelier A').first()).toBeVisible() // Client 2 await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill('Studio B') await page.getByLabel(/^Email du contact$/i).fill('b@example.com') await page.getByRole('button', { name: /^créer le client$/i }).click() await expect(page.getByText('Studio B').first()).toBeVisible() // Compteur "2 fiches" await expect(page.getByText(/·\s*2\s*fiches/i)).toBeVisible() }) test('duplicate par nom (case-insensitive) → message d\'erreur', async ({ page, }) => { await signupAndOnboard(page, 'cli-dup') await page.goto('/clients') // 1er client await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill('Atelier Durand') await page.getByLabel(/^Email du contact$/i).fill('durand@example.com') await page.getByRole('button', { name: /^créer le client$/i }).click() await expect(page.getByText('Atelier Durand').first()).toBeVisible() // Tentative duplicate (nom uppercase = même client côté API) await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill('ATELIER DURAND') await page.getByLabel(/^Email du contact$/i).fill('autre@example.com') await page.getByRole('button', { name: /^créer le client$/i }).click() // Toast / message d'erreur (l'API renvoie 409 duplicate_client) // Le dialog peut rester ouvert avec un message, ou un toast Sonner. // Test soft : la liste contient toujours 1 fiche, pas 2. await expect(page.getByText(/·\s*1\s*fiche/i)).toBeVisible({ timeout: 5_000 }) }) }) test.describe('Clients — recherche', () => { test.beforeEach(async () => { await resetDb() }) test('filtre par nom (recherche ILIKE)', async ({ page }) => { await signupAndOnboard(page, 'cli-search') await page.goto('/clients') // Crée 3 clients const clients = [ { name: 'Boulangerie Paul', email: 'paul@b.fr' }, { name: 'Atelier Durand', email: 'durand@a.fr' }, { name: 'Studio Lumière', email: 'lumiere@s.fr' }, ] for (const c of clients) { await page.getByRole('button', { name: /nouveau client/i }).first().click() await page.getByLabel(/^Nom du client$/i).fill(c.name) await page.getByLabel(/^Email du contact$/i).fill(c.email) await page.getByRole('button', { name: /^créer le client$/i }).click() await expect(page.getByText(c.name).first()).toBeVisible() } await expect(page.getByText(/·\s*3\s*fiches/i)).toBeVisible() // Recherche "boulang" → seul Paul reste await page .getByPlaceholder(/rechercher un client/i) .fill('boulang') await expect(page.getByText('Boulangerie Paul').first()).toBeVisible({ timeout: 5_000, }) await expect(page.getByText('Atelier Durand')).not.toBeVisible() await expect(page.getByText('Studio Lumière')).not.toBeVisible() }) })