rubis/e2e/tests/clients.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

168 lines
6.5 KiB
TypeScript

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()
})
})