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

137 lines
5.2 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { resetDb } from './helpers/api'
/**
* Scénarios auth : login après signup, logout, refresh session,
* redirection si non auth, validation des form errors signup/login.
*
* Le signup happy path est couvert ailleurs (signup.spec.ts) — ici on
* se concentre sur les autres flows + edge cases.
*/
async function signupAndOnboard(page: import('@playwright/test').Page) {
const email = `auth+${Date.now()}@rubis.test`
const password = 'motdepasse-fort-123'
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('Auth Test')
await page.getByLabel(/Email professionnel/i).fill(email)
await page.getByLabel(/Mot de passe/i).fill(password)
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('Auth SARL')
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, password }
}
test.describe('Auth — signup validations', () => {
test.beforeEach(async () => {
await resetDb()
})
test('refuse un email invalide (HTML5 validation)', async ({ page }) => {
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('Test')
await page.getByLabel(/Email professionnel/i).fill('pas-un-email')
await page.getByLabel(/Mot de passe/i).fill('motdepasse-fort-123')
await page.getByRole('button', { name: /créer mon compte/i }).click()
// L'input email[required] devrait être marqué invalid par le navigateur.
// On reste sur /signup (pas de navigation).
await expect(page).toHaveURL(/\/signup/)
})
test('refuse un email déjà pris (422 → toast erreur)', async ({ page }) => {
const email = `taken+${Date.now()}@rubis.test`
// 1er signup OK
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('First')
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/)
// Tentative de re-signup avec le même email → toast erreur
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('Second')
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()
// Toast Sonner avec message "Un compte existe déjà avec cet email."
await expect(page.getByText(/existe déjà avec cet email/i)).toBeVisible({
timeout: 5_000,
})
await expect(page).toHaveURL(/\/signup/)
})
})
test.describe('Auth — login', () => {
test.beforeEach(async () => {
await resetDb()
})
test('login après signup → dashboard', async ({ page, context }) => {
const { email, password } = await signupAndOnboard(page)
// Clear cookies + open new context-like session pour forcer un re-login
await context.clearCookies()
await page.goto('/login')
await page.getByLabel(/^Email$/i).fill(email)
await page.getByLabel(/^Mot de passe$/i).fill(password)
await page.getByRole('button', { name: /^se connecter$/i }).click()
await expect(page).toHaveURL('/', { timeout: 10_000 })
await expect(page.getByText(/rubis.*gagnés/i)).toBeVisible({ timeout: 10_000 })
})
test('mauvais password → 401 + toast erreur, reste sur /login', async ({
page,
}) => {
const { email } = await signupAndOnboard(page)
await page.goto('/login')
await page.getByLabel(/^Email$/i).fill(email)
await page.getByLabel(/^Mot de passe$/i).fill('mauvais-mot-de-passe')
await page.getByRole('button', { name: /^se connecter$/i }).click()
await expect(
page.getByText(/email ou mot de passe incorrect/i),
).toBeVisible({ timeout: 5_000 })
await expect(page).toHaveURL(/\/login/)
})
test('email inconnu → 401 + toast erreur', async ({ page }) => {
await page.goto('/login')
await page.getByLabel(/^Email$/i).fill('inconnu@nulle-part.test')
await page.getByLabel(/^Mot de passe$/i).fill('peu-importe-fort-123')
await page.getByRole('button', { name: /^se connecter$/i }).click()
await expect(
page.getByText(/email ou mot de passe incorrect/i),
).toBeVisible({ timeout: 5_000 })
})
})
test.describe('Auth — protection des routes', () => {
test.beforeEach(async () => {
await resetDb()
})
test('accéder à / sans session → redirige sur /login', async ({ page, context }) => {
await context.clearCookies()
await page.goto('/')
await expect(page).toHaveURL(/\/login/, { timeout: 5_000 })
})
test('accéder à /factures sans session → redirige sur /login', async ({
page,
context,
}) => {
await context.clearCookies()
await page.goto('/factures')
await expect(page).toHaveURL(/\/login/, { timeout: 5_000 })
})
})