All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 28s
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>
152 lines
5.5 KiB
TypeScript
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 })
|
|
})
|
|
})
|