rubis/e2e/tests/billing-quota.spec.ts
ordinarthur e40f417caa
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 28s
test(e2e): PR 3 — billing states + quota + dashboard + settings (+ fix isDirty)
Couvre les surfaces transverses post auth/clients/factures :

- billing-states (3) : transitions webhook trial→active, past_due, cancel
- billing-quota (2) : Free limite à 2 factures actives, 3e bloquée + toast
  remonté avec message API (UX bug : onError du dialog masquait l'erreur)
- dashboard (2) : zéros au start, +rubis et activity feed après mark-paid
- settings (2) : sections visibles + persistence Prénom/Nom après reload

Bug isDirty détecté par TDD sur settings : AccountForm/OrganizationForm/
SignatureForm lisaient form.state.isDirty *hors* d'un form.Subscribe, donc
le bouton Save ne réagissait jamais aux changements (texte figé sur "Aucune
modification"). Fix : wrap le bouton dans form.Subscribe selector=isDirty,
même pattern que ManualInvoiceDialog.

36 tests Playwright vert, ~1m20.

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

152 lines
5.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { resetDb } from './helpers/api'
/**
* Scénario quota Free 2 factures (cf. ADR-023).
*
* Nouveau user → org en Free pure (pas de grace_period_ends_at posé sur
* les nouvelles orgs, cf. billing.ts §contexte). À la 3e facture active,
* la création doit être bloquée par `canCreateInvoices` → 402
* plan_limit_reached côté API.
*
* On valide ici la chaîne : SPA → API → DB. Les tests unit billing.spec.ts
* couvrent les variations détaillées (delta, statuts).
*/
async function signupAndOnboard(page: import('@playwright/test').Page) {
const email = `quota+${Date.now()}@rubis.test`
await page.goto('/signup')
await page.getByLabel(/Prénom \/ Nom/i).fill('Quota Test')
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 Quota')
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 })
}
async function createInvoiceForNewClient(
page: import('@playwright/test').Page,
opts: { clientName: string; clientEmail: string; numero: string; amount: string },
) {
await page.getByRole('button', { name: /^saisir$/i }).first().click()
await expect(page.getByRole('dialog')).toBeVisible()
const combobox = page.getByPlaceholder(/rechercher.*créer un client/i)
await combobox.fill(opts.clientName)
// Une fois 1er client créé, il apparaît dans le dropdown — on prend
// soit l'option "Créer le client" pour un nouveau, soit l'option
// matchée pour un existant.
const createOption = page.getByRole('button', {
name: new RegExp(`créer le client.*${opts.clientName}`, 'i'),
})
const existingOption = page.getByRole('option', {
name: new RegExp(opts.clientName, 'i'),
})
// Race : on prend ce qui apparaît en premier
await Promise.race([
createOption.waitFor({ state: 'visible', timeout: 3_000 }).then(() => createOption.click()),
existingOption.waitFor({ state: 'visible', timeout: 3_000 }).then(() => existingOption.click()),
])
// Le champ email apparaît si c'est un nouveau client
const emailField = page.getByLabel(/email du client/i)
if (await emailField.isVisible().catch(() => false)) {
await emailField.fill(opts.clientEmail)
}
await page.getByLabel(/N° de facture/i).fill(opts.numero)
await page.getByLabel(/Montant TTC/i).fill(opts.amount)
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()
}
test.describe('Billing — quota Free 2 factures', () => {
test.beforeEach(async () => {
await resetDb()
})
test('Free post-grace : 2 factures OK, la 3e est bloquée avec message clair', async ({
page,
}) => {
await signupAndOnboard(page)
// 1re facture OK
await createInvoiceForNewClient(page, {
clientName: 'Client Quota A',
clientEmail: 'a@quota.test',
numero: 'F-QUOTA-1',
amount: '100',
})
await expect(page.getByText(/facture créée/i)).toBeVisible({ timeout: 5_000 })
// 2e OK (on a maintenant 2 factures actives sur 2)
await createInvoiceForNewClient(page, {
clientName: 'Client Quota B',
clientEmail: 'b@quota.test',
numero: 'F-QUOTA-2',
amount: '200',
})
await expect(page.getByText(/facture créée/i).first()).toBeVisible({
timeout: 5_000,
})
// 3e doit être bloquée (Free 2 max, post-grace)
await createInvoiceForNewClient(page, {
clientName: 'Client Quota C',
clientEmail: 'c@quota.test',
numero: 'F-QUOTA-3',
amount: '300',
})
// Toast d'erreur "Limite atteinte : 2 factures actives sur le plan Free"
await expect(
page.getByText(/limite.*atteinte|2 factures.*Free|passez pro/i).first(),
).toBeVisible({ timeout: 5_000 })
// La facture F-QUOTA-3 ne doit PAS apparaître dans /factures
await page.goto('/factures')
await expect(page.getByText('F-QUOTA-1').first()).toBeVisible()
await expect(page.getByText('F-QUOTA-2').first()).toBeVisible()
await expect(page.getByText('F-QUOTA-3')).not.toBeVisible()
})
test('Banner "Limite Free atteinte" apparaît sur /factures à 2/2', async ({
page,
}) => {
await signupAndOnboard(page)
// Créer 2 factures pour atteindre la limite
await createInvoiceForNewClient(page, {
clientName: 'Banner A',
clientEmail: 'banner-a@quota.test',
numero: 'F-BAN-1',
amount: '100',
})
await expect(page.getByText(/facture créée/i)).toBeVisible({ timeout: 5_000 })
await createInvoiceForNewClient(page, {
clientName: 'Banner B',
clientEmail: 'banner-b@quota.test',
numero: 'F-BAN-2',
amount: '200',
})
await expect(page.getByText(/facture créée/i).first()).toBeVisible({
timeout: 5_000,
})
// Sur /factures, le banner doit afficher "Limite Free atteinte"
await page.goto('/factures')
await expect(
page.getByText(/limite Free atteinte|Passer Pro/i).first(),
).toBeVisible({ timeout: 5_000 })
})
})