test(web): e2e profile flow with BMR/TDEE assertions
- e2e/profile.spec.ts: fresh user signs up, opens /profile via the home CTA, fills the Michele PRD persona values (49, MALE, 170cm, 76kg, SEDENTARY) and asserts the summary panel shows BMR 1583 kcal and TDEE 1899 kcal - profile-form.tsx: each Field receives an explicit `id` so <Label htmlFor> binds correctly — Playwright's getByLabel and screen-reader navigation both rely on this association Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5b38ddc41d
commit
4349c395e3
2 files changed
+79
-13
No files matched your search
@@ -0,0 +1,40 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
function uniqueEmail(): string {
|
||||||
|
const ts = Date.now();
|
||||||
|
const rand = Math.floor(Math.random() * 1e6);
|
||||||
|
return `e2e-profile-${ts}-${rand}@ketopath.test`;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('profile flow — submit returns BMR/TDEE for the Michele PRD persona', async ({ page }) => {
|
||||||
|
const email = uniqueEmail();
|
||||||
|
const password = 'Password!123';
|
||||||
|
const name = 'Michele E2E';
|
||||||
|
|
||||||
|
// Sign-up gives us an authenticated session
|
||||||
|
await page.goto('/sign-up');
|
||||||
|
await page.getByLabel('Nome').fill(name);
|
||||||
|
await page.getByLabel('Email').fill(email);
|
||||||
|
await page.getByLabel('Password').fill(password);
|
||||||
|
await page.getByRole('button', { name: 'Crea account' }).click();
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
|
// Open the profile page from the home CTA
|
||||||
|
await page.getByRole('link', { name: 'Completa il tuo profilo' }).click();
|
||||||
|
await page.waitForURL('/profile');
|
||||||
|
|
||||||
|
// Fill the form with the "Michele" PRD persona values
|
||||||
|
await page.getByLabel('Età').fill('49');
|
||||||
|
await page.getByLabel('Sesso').selectOption('MALE');
|
||||||
|
await page.getByLabel('Altezza (cm)').fill('170');
|
||||||
|
await page.getByLabel('Peso iniziale (kg)').fill('76');
|
||||||
|
await page.getByLabel('Peso attuale (kg)').fill('76');
|
||||||
|
await page.getByLabel('Peso obiettivo (kg)').fill('70');
|
||||||
|
await page.getByLabel('Livello di attività').selectOption('SEDENTARY');
|
||||||
|
await page.getByRole('button', { name: 'Salva profilo' }).click();
|
||||||
|
|
||||||
|
// BMR/TDEE summary panel becomes visible after a successful save
|
||||||
|
await expect(page.getByText('Calcolo personalizzato')).toBeVisible();
|
||||||
|
await expect(page.getByText('1583 kcal')).toBeVisible(); // BMR Michele
|
||||||
|
await expect(page.getByText('1899 kcal')).toBeVisible(); // TDEE = 1582.5 × 1.2
|
||||||
|
});
|
||||||
@@ -58,11 +58,12 @@ export function ProfileForm({ initial }: { initial: Profile | null }) {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="space-y-5" noValidate>
|
<form onSubmit={onSubmit} className="space-y-5" noValidate>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Field label={t('age')} error={errors.age?.message}>
|
<Field id="age" label={t('age')} error={errors.age?.message}>
|
||||||
<Input type="number" inputMode="numeric" {...register('age')} />
|
<Input id="age" type="number" inputMode="numeric" {...register('age')} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('gender')} error={errors.gender?.message}>
|
<Field id="gender" label={t('gender')} error={errors.gender?.message}>
|
||||||
<select
|
<select
|
||||||
|
id="gender"
|
||||||
className="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
{...register('gender')}
|
{...register('gender')}
|
||||||
>
|
>
|
||||||
@@ -76,24 +77,47 @@ export function ProfileForm({ initial }: { initial: Profile | null }) {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field label={t('heightCm')} error={errors.heightCm?.message}>
|
<Field id="heightCm" label={t('heightCm')} error={errors.heightCm?.message}>
|
||||||
<Input type="number" inputMode="numeric" {...register('heightCm')} />
|
<Input id="heightCm" type="number" inputMode="numeric" {...register('heightCm')} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<Field label={t('weightStartKg')} error={errors.weightStartKg?.message}>
|
<Field id="weightStartKg" label={t('weightStartKg')} error={errors.weightStartKg?.message}>
|
||||||
<Input type="number" step="0.1" inputMode="decimal" {...register('weightStartKg')} />
|
<Input
|
||||||
|
id="weightStartKg"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
inputMode="decimal"
|
||||||
|
{...register('weightStartKg')}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('weightCurrentKg')} error={errors.weightCurrentKg?.message}>
|
<Field
|
||||||
<Input type="number" step="0.1" inputMode="decimal" {...register('weightCurrentKg')} />
|
id="weightCurrentKg"
|
||||||
|
label={t('weightCurrentKg')}
|
||||||
|
error={errors.weightCurrentKg?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="weightCurrentKg"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
inputMode="decimal"
|
||||||
|
{...register('weightCurrentKg')}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('weightGoalKg')} error={errors.weightGoalKg?.message}>
|
<Field id="weightGoalKg" label={t('weightGoalKg')} error={errors.weightGoalKg?.message}>
|
||||||
<Input type="number" step="0.1" inputMode="decimal" {...register('weightGoalKg')} />
|
<Input
|
||||||
|
id="weightGoalKg"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
inputMode="decimal"
|
||||||
|
{...register('weightGoalKg')}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field label={t('activityLevel')} error={errors.activityLevel?.message}>
|
<Field id="activityLevel" label={t('activityLevel')} error={errors.activityLevel?.message}>
|
||||||
<select
|
<select
|
||||||
|
id="activityLevel"
|
||||||
className="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
{...register('activityLevel')}
|
{...register('activityLevel')}
|
||||||
>
|
>
|
||||||
@@ -139,17 +163,19 @@ export function ProfileForm({ initial }: { initial: Profile | null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Field({
|
function Field({
|
||||||
|
id,
|
||||||
label,
|
label,
|
||||||
error,
|
error,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>{label}</Label>
|
<Label htmlFor={id}>{label}</Label>
|
||||||
{children}
|
{children}
|
||||||
{error ? <p className="text-destructive text-xs">{error}</p> : null}
|
{error ? <p className="text-destructive text-xs">{error}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user