Skip to content

Commit 3301f43

Browse files
fix(aluno): finish-button hint + duration clamp (v2, rebased on #182) (#184)
* fix(aluno): hint when finish button disabled by zero checks CardTreino footer ganha <p> condicional data-testid finish-hint quando completedCount === 0 e sem feedback loading. Sinaliza pre-condicao (marque exercicios) para o botao Finalizar e Avaliar Treino que parecia morto quando disabled. Test: stateful mock useWorkoutTracker para toggle. 15/15 card-treino, 1134/1134 suite. Co-Authored-By: Claude <noreply@anthropic.com> * fix(workout): clamp duracaoMinutos to min 1 for Zod schema HistoricoTreinoBaseSchema requires duracaoMinutos >= 1. Quick workouts (<30s) rounded to 0, failing save and surfacing "ocorreu um erro inesperado ao salvar no histórico" toast. Co-Authored-By: Claude <noreply@anthropic.com> * fix(aluno): address cubic P2 — aria-live, duration test, simpler mock - card-treino.tsx: finish-hint gains aria-live="polite" so AT announces reappearance when user unchecks last completed exercise. - WorkoutSession.test.tsx: new test asserting onFinish receives duracaoMinutos >= 1 even on sub-minute session (regression guard for the Math.max(1, ...) clamp). - card-treino.test.tsx: simplify "hides finish hint" test per cubic — set mockReturnValue directly to { 'ex-1': true } + rerender; drop mutable let, callback re-mock, fireEvent.click, silent no-op path. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Emiya Kiritsugu <emiyakiritsugu3@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 005f66e commit 3301f43

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

src/components/WorkoutSession.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ describe('WorkoutSession', () => {
195195
});
196196
});
197197

198+
it('clamps duracaoMinutos to at least 1 on a very short session', async () => {
199+
const { generateWorkoutFeedback } = await import('@/ai/flows/workout-feedback-flow');
200+
vi.mocked(generateWorkoutFeedback).mockResolvedValue({
201+
title: 'Treino Concluído!',
202+
message: 'Excelente trabalho!',
203+
});
204+
205+
render(<WorkoutSession treino={mockTreino} onFinish={mockOnFinish} onCancel={mockOnCancel} />);
206+
// Finish immediately — elapsed < 1 min, raw Math.round would yield 0
207+
fireEvent.click(screen.getByText(/Próximo/));
208+
fireEvent.click(screen.getByText(/Finalizar Treino/));
209+
210+
await waitFor(() => {
211+
expect(mockOnFinish).toHaveBeenCalled();
212+
});
213+
const historico = mockOnFinish.mock.calls[0][0];
214+
expect(historico.duracaoMinutos).toBeGreaterThanOrEqual(1);
215+
});
216+
198217
it('shows loading state while generating feedback', async () => {
199218
const { generateWorkoutFeedback } = await import('@/ai/flows/workout-feedback-flow');
200219
let resolveFeedback!: (v: { title: string; message: string }) => void;

src/components/WorkoutSession.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ export function WorkoutSession({ treino, onFinish, onCancel }: Readonly<WorkoutS
170170
if (isFinishing) return;
171171
setIsFinishing(true);
172172
const endTime = new Date();
173-
const duracaoMinutos = Math.round((endTime.getTime() - startTime.getTime()) / 60000);
173+
const duracaoMinutos = Math.max(
174+
1,
175+
Math.round((endTime.getTime() - startTime.getTime()) / 60000)
176+
);
174177

175178
const historico: Omit<HistoricoTreino, 'id' | 'alunoId'> = {
176179
treinoId: treino.id,

src/components/dashboard/aluno/card-treino.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,47 @@ describe('CardTreino', () => {
212212
expect(button?.disabled).toBe(true);
213213
});
214214

215+
it('shows finish hint when no exercises checked', () => {
216+
render(
217+
<CardTreino
218+
treino={mockTreino}
219+
onFinishTraining={mockOnFinishTraining}
220+
isFeedbackLoading={false}
221+
onViewExercicio={mockOnViewExercicio}
222+
/>
223+
);
224+
expect(screen.getByTestId('finish-hint')).toBeTruthy();
225+
expect(
226+
screen.getByText('Marque os exercícios concluídos acima para finalizar o treino.')
227+
).toBeTruthy();
228+
});
229+
230+
it('hides finish hint when an exercise is checked', () => {
231+
const { rerender } = render(
232+
<CardTreino
233+
treino={mockTreino}
234+
onFinishTraining={mockOnFinishTraining}
235+
isFeedbackLoading={false}
236+
onViewExercicio={mockOnViewExercicio}
237+
/>
238+
);
239+
expect(screen.getByTestId('finish-hint')).toBeTruthy();
240+
241+
mockUseWorkoutTracker.mockReturnValue({
242+
checkedExercises: { 'ex-1': true },
243+
handleCheckChange: vi.fn(),
244+
});
245+
rerender(
246+
<CardTreino
247+
treino={mockTreino}
248+
onFinishTraining={mockOnFinishTraining}
249+
isFeedbackLoading={false}
250+
onViewExercicio={mockOnViewExercicio}
251+
/>
252+
);
253+
expect(screen.queryByTestId('finish-hint')).toBeNull();
254+
});
255+
215256
it('shows loading state when feedback is loading', () => {
216257
render(
217258
<CardTreino

src/components/dashboard/aluno/card-treino.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function CardTreino({
123123
</div>
124124
))}
125125
</CardContent>
126-
<CardFooter className="bg-white/5 p-6 border-t border-white/5">
126+
<CardFooter className="bg-white/5 p-6 border-t border-white/5 flex flex-col gap-3">
127127
<Button
128128
variant="premium"
129129
size="lg"
@@ -143,6 +143,15 @@ export function CardTreino({
143143
</div>
144144
)}
145145
</Button>
146+
{completedCount === 0 && !isFeedbackLoading && (
147+
<p
148+
data-testid="finish-hint"
149+
aria-live="polite"
150+
className="text-sm text-muted-foreground text-center"
151+
>
152+
Marque os exercícios concluídos acima para finalizar o treino.
153+
</p>
154+
)}
146155
</CardFooter>
147156
</Card>
148157
);

0 commit comments

Comments
 (0)