test(e2e): PR 3 — billing states + quota + dashboard + settings (+ fix isDirty)
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 28s
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>
This commit is contained in:
parent
1e2eecdeba
commit
e40f417caa
@ -131,8 +131,15 @@ export function ManualInvoiceDialog({ open, onOpenChange }: ManualInvoiceDialogP
|
||||
toast.success("Facture créée. + 1 rubis.");
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Création impossible. Vérifiez les champs.");
|
||||
onError: (err) => {
|
||||
// Remonte le message de l'API si on l'a (ex. quota Free atteint,
|
||||
// duplicate, validation). Sinon toast générique.
|
||||
// Le `ApiError` de @/lib/api expose `.message` directement.
|
||||
const message =
|
||||
err instanceof Error && err.message
|
||||
? err.message
|
||||
: "Création impossible. Vérifiez les champs.";
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -118,16 +118,20 @@ export function AccountForm() {
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!form.state.isDirty}
|
||||
>
|
||||
{form.state.isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
<form.Subscribe selector={(state) => state.isDirty}>
|
||||
{(isDirty) => (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -175,16 +175,20 @@ export function OrganizationForm() {
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!form.state.isDirty}
|
||||
>
|
||||
{form.state.isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
<form.Subscribe selector={(state) => state.isDirty}>
|
||||
{(isDirty) => (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -102,16 +102,20 @@ export function SignatureForm() {
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!form.state.isDirty}
|
||||
>
|
||||
{form.state.isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
<form.Subscribe selector={(state) => state.isDirty}>
|
||||
{(isDirty) => (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
loading={updateMutation.isPending}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isDirty ? "Enregistrer" : "Aucune modification"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
151
e2e/tests/billing-quota.spec.ts
Normal file
151
e2e/tests/billing-quota.spec.ts
Normal file
@ -0,0 +1,151 @@
|
||||
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 })
|
||||
})
|
||||
})
|
||||
141
e2e/tests/billing-states.spec.ts
Normal file
141
e2e/tests/billing-states.spec.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { fireStripeWebhook, getLastOrg, resetDb } from './helpers/api'
|
||||
|
||||
/**
|
||||
* Scénarios billing — transitions de subscription côté DB après les
|
||||
* différents webhooks Stripe (au-delà de l'essai 14 j déjà couvert
|
||||
* dans billing-trial.spec.ts).
|
||||
*
|
||||
* On teste les états observables :
|
||||
* - subscription.updated trialing → active (J+14 paiement OK)
|
||||
* - subscription.updated trialing → past_due (paiement échoué)
|
||||
* - subscription.deleted → org bascule en Free
|
||||
* - invoice.payment_failed → past_due
|
||||
*
|
||||
* Le rendu SPA du status est testé séparément par les tests vitest
|
||||
* (useSubscription / SubscriptionState).
|
||||
*/
|
||||
|
||||
async function signupAndStartTrial(page: import('@playwright/test').Page, tag: string) {
|
||||
const email = `${tag}+${Date.now()}@rubis.test`
|
||||
await page.goto('/signup')
|
||||
await page.getByLabel(/Prénom \/ Nom/i).fill('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 })
|
||||
|
||||
// Onboarding pour avoir un user complet
|
||||
await page.getByRole('button', { name: /continuer/i }).click()
|
||||
await page.waitForURL(/\/onboarding\/entreprise/)
|
||||
await page.getByLabel(/nom de l'entreprise/i).fill('Atelier Billing')
|
||||
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 })
|
||||
|
||||
// Démarrer l'essai 14 j (pose stripeCustomerId + envoie redirect)
|
||||
await page.goto('/onboarding/billing')
|
||||
await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click()
|
||||
await page.waitForURL(/\/onboarding\/compte\?trial=started/, { timeout: 10_000 })
|
||||
|
||||
// Webhook checkout.completed → org en trialing avec sub_e2e_mock posé
|
||||
const org = await getLastOrg()
|
||||
await fireStripeWebhook({
|
||||
type: 'checkout.session.completed',
|
||||
data: {
|
||||
object: {
|
||||
id: 'cs_e2e_mock',
|
||||
subscription: 'sub_e2e_mock',
|
||||
metadata: { organization_id: org!.id },
|
||||
},
|
||||
},
|
||||
})
|
||||
return org!
|
||||
}
|
||||
|
||||
test.describe('Billing — transitions de subscription via webhooks', () => {
|
||||
test.beforeEach(async () => {
|
||||
await resetDb()
|
||||
})
|
||||
|
||||
test('trial → active : org reste pro avec status active', async ({ page }) => {
|
||||
const orgBefore = await signupAndStartTrial(page, 'bill-active')
|
||||
expect((await getLastOrg())?.subscription_status).toBe('trialing')
|
||||
|
||||
// Stripe envoie customer.subscription.updated (status='active') à J+14
|
||||
await fireStripeWebhook({
|
||||
type: 'customer.subscription.updated',
|
||||
data: {
|
||||
object: {
|
||||
id: 'sub_e2e_mock',
|
||||
customer: 'cus_e2e_mock',
|
||||
status: 'active',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
id: 'si_e2e',
|
||||
price: { id: 'price_pro_monthly_e2e', lookup_key: 'rubis_pro_monthly' },
|
||||
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 3600,
|
||||
},
|
||||
],
|
||||
},
|
||||
cancel_at_period_end: false,
|
||||
metadata: { organization_id: orgBefore.id },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const after = await getLastOrg()
|
||||
expect(after?.subscription_status).toBe('active')
|
||||
expect(after?.plan).toBe('pro')
|
||||
})
|
||||
|
||||
test('invoice.payment_failed → org passe en past_due (le plan reste Pro)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await signupAndStartTrial(page, 'bill-pastdue')
|
||||
|
||||
await fireStripeWebhook({
|
||||
type: 'invoice.payment_failed',
|
||||
data: {
|
||||
object: {
|
||||
id: 'in_e2e_failed',
|
||||
customer: 'cus_e2e_mock',
|
||||
status: 'open',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const after = await getLastOrg()
|
||||
expect(after?.subscription_status).toBe('past_due')
|
||||
// Important : le plan reste Pro (Stripe smart retries = 7 j de tolérance)
|
||||
expect(after?.plan).toBe('pro')
|
||||
})
|
||||
|
||||
test('subscription.deleted → org bascule en Free, trial_ends_at conservé', async ({
|
||||
page,
|
||||
}) => {
|
||||
const orgBefore = await signupAndStartTrial(page, 'bill-deleted')
|
||||
const trialEndsAtBefore = (await getLastOrg())?.trial_ends_at
|
||||
|
||||
await fireStripeWebhook({
|
||||
type: 'customer.subscription.deleted',
|
||||
data: {
|
||||
object: {
|
||||
id: 'sub_e2e_mock',
|
||||
customer: 'cus_e2e_mock',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const after = await getLastOrg()
|
||||
expect(after?.id).toBe(orgBefore.id)
|
||||
expect(after?.plan).toBe('free')
|
||||
expect(after?.subscription_status).toBe('canceled')
|
||||
expect(after?.stripe_subscription_id).toBeNull()
|
||||
// trial_ends_at conservé pour l'historique (empêche un re-trial)
|
||||
expect(after?.trial_ends_at).toBeTruthy()
|
||||
expect(after?.trial_ends_at).toBe(trialEndsAtBefore)
|
||||
})
|
||||
})
|
||||
85
e2e/tests/dashboard.spec.ts
Normal file
85
e2e/tests/dashboard.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { resetDb } from './helpers/api'
|
||||
|
||||
/**
|
||||
* Scénarios dashboard : KPIs à zéro au start, bump après actions.
|
||||
*
|
||||
* Les calculs des KPIs sont testés par les tests japa functional
|
||||
* (dashboard.spec.ts) — ici on valide la chaîne SPA → API → render
|
||||
* sur la page d'accueil.
|
||||
*/
|
||||
|
||||
async function signupAndOnboard(page: import('@playwright/test').Page) {
|
||||
const email = `dash+${Date.now()}@rubis.test`
|
||||
await page.goto('/signup')
|
||||
await page.getByLabel(/Prénom \/ Nom/i).fill('Dashboard 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 Dashboard')
|
||||
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 })
|
||||
}
|
||||
|
||||
test.describe('Dashboard', () => {
|
||||
test.beforeEach(async () => {
|
||||
await resetDb()
|
||||
})
|
||||
|
||||
test('zéros au start : 0 rubis, "Pas encore d\'activité"', async ({ page }) => {
|
||||
await signupAndOnboard(page)
|
||||
await page.goto('/')
|
||||
|
||||
// RubisHero : "0 rubis gagnés ce mois"
|
||||
await expect(page.getByText(/0\s+rubis/i).first()).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Activity feed vide
|
||||
await expect(
|
||||
page.getByText(/pas encore d'activité|importez votre première facture/i).first(),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('après mark-paid : rubis += 1 + activity contient l\'event', async ({
|
||||
page,
|
||||
}) => {
|
||||
await signupAndOnboard(page)
|
||||
|
||||
// 1. Créer client + facture
|
||||
await page.getByRole('button', { name: /^saisir$/i }).first().click()
|
||||
const combobox = page.getByPlaceholder(/rechercher.*créer un client/i)
|
||||
await combobox.fill('Dashboard Client')
|
||||
await page.getByRole('button', { name: /créer le client.*dashboard client/i }).click()
|
||||
await page.getByLabel(/email du client/i).fill('client@dashboard.test')
|
||||
await page.getByLabel(/N° de facture/i).fill('F-DASH-001')
|
||||
await page.getByLabel(/Montant TTC/i).fill('500')
|
||||
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()
|
||||
await expect(page.getByText(/facture créée/i)).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// 2. Aller sur la facture, mark-paid
|
||||
await page.goto('/factures')
|
||||
await page.getByText('F-DASH-001').first().click()
|
||||
await page.getByRole('button', { name: /marquer encaissée/i }).click()
|
||||
await expect(page.getByText(/encaissée.*rubis/i)).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// 3. Sur le dashboard, rubis count = 1 (création) + 1 (mark-paid) = 2 rubis
|
||||
// Note : la création d'une facture saisie manuelle pose rubisEarned=1
|
||||
// et le mark-paid bump encore +1
|
||||
await page.goto('/')
|
||||
// Le compteur RubisHero affiche "X rubis gagnés" et la sidebar aussi
|
||||
await expect(page.getByText(/[1-2]\s+rubis/i).first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// 4. Activity feed contient l'event "Facture F-DASH-001 marquée encaissée"
|
||||
await expect(page.getByText(/F-DASH-001/).first()).toBeVisible({
|
||||
timeout: 5_000,
|
||||
})
|
||||
})
|
||||
})
|
||||
74
e2e/tests/settings.spec.ts
Normal file
74
e2e/tests/settings.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { resetDb } from './helpers/api'
|
||||
|
||||
/**
|
||||
* Scénarios /parametres : forms account + organization + signature.
|
||||
*
|
||||
* Chaque form est isolé (son propre Save button). On valide la
|
||||
* persistence après reload page — pas juste l'UI optimiste.
|
||||
*/
|
||||
|
||||
async function signupAndOnboard(page: import('@playwright/test').Page) {
|
||||
const email = `set+${Date.now()}@rubis.test`
|
||||
await page.goto('/signup')
|
||||
await page.getByLabel(/Prénom \/ Nom/i).fill('Settings 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 Settings')
|
||||
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 })
|
||||
}
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test.beforeEach(async () => {
|
||||
await resetDb()
|
||||
})
|
||||
|
||||
test('page /parametres affiche les sections principales', async ({ page }) => {
|
||||
await signupAndOnboard(page)
|
||||
await page.goto('/parametres')
|
||||
|
||||
await expect(page.getByRole('heading', { name: /paramètres/i })).toBeVisible()
|
||||
|
||||
// Les sections AccountForm, OrganizationForm, SignatureForm sont
|
||||
// toutes visibles (rendues par /parametres). Le scroll est long mais
|
||||
// tous les labels sont dans le DOM dès le mount.
|
||||
await expect(page.getByText(/^compte$/i).first()).toBeVisible()
|
||||
await expect(page.getByText(/^entreprise$/i).first()).toBeVisible()
|
||||
await expect(page.getByText(/signature email/i).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('modifier "Prénom et nom" → toast + persistence après reload', async ({
|
||||
page,
|
||||
}) => {
|
||||
await signupAndOnboard(page)
|
||||
await page.goto('/parametres')
|
||||
|
||||
const fullNameInput = page.getByLabel(/^Prénom et nom$/i)
|
||||
await expect(fullNameInput).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// L'initial du onboarding était "Settings Test"
|
||||
await expect(fullNameInput).toHaveValue('Settings Test')
|
||||
|
||||
await fullNameInput.fill('Pierre Modifié')
|
||||
|
||||
// Le bouton passe de "Aucune modification" à "Enregistrer"
|
||||
const saveButton = page.getByRole('button', { name: /^enregistrer$/i }).first()
|
||||
await saveButton.click()
|
||||
|
||||
// Confirmation : le bouton revient à "Aucune modification" après save
|
||||
await expect(
|
||||
page.getByRole('button', { name: /aucune modification/i }).first(),
|
||||
).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// Reload : la valeur persiste
|
||||
await page.reload()
|
||||
await expect(page.getByLabel(/^Prénom et nom$/i)).toHaveValue('Pierre Modifié')
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user