trainlog/web/src/routes/AnalysisPage.test.tsx

196 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AnalysisPage } from "./AnalysisPage";
import { analysisFixture } from "../api/analysis.test";
import type { AnalysisSnapshot } from "../api/analysis";
describe("AnalysisPage", () => {
beforeEach(() => {
document.documentElement.lang = "fr";
window.history.replaceState(
null,
"",
"/analyse?section=invalid&period=broken&metric=unknown&exercise_id=nope",
);
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(analysisFixture), { status: 200 }),
);
});
afterEach(() => vi.restoreAllMocks());
it("falls back safely and changes sections, periods and metrics", async () => {
render(<AnalysisPage />);
expect(
await screen.findByRole("heading", { name: "Analyse" }),
).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Vue d’ensemble" })).toHaveAttribute(
"aria-selected",
"true",
);
expect(screen.getByLabelText("Période")).toHaveValue("30d");
fireEvent.change(screen.getByLabelText("Période"), {
target: { value: "7d" },
});
await waitFor(() =>
expect(globalThis.fetch).toHaveBeenLastCalledWith(
expect.stringContaining("period=7d"),
expect.anything(),
),
);
fireEvent.click(screen.getByRole("tab", { name: "Mensurations" }));
fireEvent.change(screen.getByLabelText("Mesure"), {
target: { value: "waist" },
});
expect(window.location.search).toContain("section=measurements");
});
it("supports exercise and BODY ZONE deep-link sections without duplicating the global language control", async () => {
window.history.replaceState(
null,
"",
`/analyse?section=exercise&exercise_id=${analysisFixture.exercises[0].exercise_id}&period=30d`,
);
render(<AnalysisPage />);
expect(
await screen.findByRole("heading", { name: "Presse" }),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("tab", { name: "Répartition" }));
expect(screen.getByText(/pourcentages physiologiques/)).toBeInTheDocument();
expect(screen.queryByLabelText("Langue")).not.toBeInTheDocument();
expect(document.querySelector(".analysis-zone-track span")).toHaveStyle({
width: "100%",
});
expect(document.body).not.toHaveTextContent("%");
});
it("humanizes durations and hides set counts for a continuous exercise", async () => {
const continuous: AnalysisSnapshot = {
...analysisFixture,
exercises: [
{
...analysisFixture.exercises[0],
name: "Marche",
tracking_mode: "duration",
recording_mode: "continuous",
},
],
exercise: {
...analysisFixture.exercise!,
name: "Marche",
tracking_mode: "duration",
recording_mode: "continuous",
points: [
{
...analysisFixture.exercise!.points[0],
sets: 0,
reps: null,
external_load_kg: null,
external_volume_kg: null,
explicit_max_kg: null,
continuous_duration_seconds: 900,
distance_km: 1.43,
speed_kmh: 6.4,
},
],
},
};
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify(continuous), { status: 200 }),
);
window.history.replaceState(
null,
"",
`/analyse?section=exercise&exercise_id=${continuous.exercises[0].exercise_id}&period=30d`,
);
render(<AnalysisPage />);
expect(
await screen.findByRole("heading", { name: "Marche" }),
).toBeInTheDocument();
expect(screen.getByText("15 min")).toBeInTheDocument();
expect(screen.getByText("1,43 km")).toBeInTheDocument();
expect(screen.getByText("6,4 km/h")).toBeInTheDocument();
expect(screen.queryByText("Séries")).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Durée" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Distance" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Vitesse" })).toBeInTheDocument();
expect(
screen.queryByRole("tab", { name: "Charge" }),
).not.toBeInTheDocument();
expect(
screen.getByText(/Durée · min · 1 points réels/),
).toBeInTheDocument();
});
it("offers only available loaded-repetition metrics and labels chart axes", async () => {
window.history.replaceState(
null,
"",
`/analyse?section=exercise&exercise_id=${analysisFixture.exercises[0].exercise_id}&period=30d`,
);
render(<AnalysisPage />);
expect(await screen.findByRole("tab", { name: "Charge" })).toHaveAttribute(
"aria-selected",
"true",
);
expect(
screen.getByRole("tab", { name: "Répétitions" }),
).toBeInTheDocument();
expect(
screen.getByRole("tab", { name: "Volume externe" }),
).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "MAX" })).toBeInTheDocument();
expect(
screen.queryByRole("tab", { name: "Durée" }),
).not.toBeInTheDocument();
expect(
screen.getByText(/Charge · kg · 1 points réels/),
).toBeInTheDocument();
expect(document.querySelectorAll(".chart-x-label")).toHaveLength(1);
expect(document.querySelectorAll(".chart-y-label")).toHaveLength(2);
});
it("renders a neutral factual measurement delta with first and last dates", async () => {
const summaries = analysisFixture.measurements.summaries.map((summary) =>
summary.metric === "weight"
? { ...summary, count: 2, first: 83.7, last: 85.1, delta: 1.4 }
: summary,
);
const measurements: AnalysisSnapshot = {
...analysisFixture,
measurements: {
selected_metric: "weight",
unit: "kg",
summaries,
points: [
{ timestamp: "2026-08-20T08:00:00Z", value: 83.7 },
{ timestamp: "2026-09-20T08:00:00Z", value: 85.1 },
],
},
};
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify(measurements), { status: 200 }),
);
window.history.replaceState(
null,
"",
"/analyse?section=measurements&metric=weight&period=30d",
);
render(<AnalysisPage />);
const delta = await screen.findByText("+1,4 kg");
expect(delta.parentElement).toHaveClass("analysis-delta");
expect(screen.getByText(/Poids · kg · 2 points réels/)).toBeInTheDocument();
expect(document.querySelectorAll(".chart-x-label")).toHaveLength(2);
});
it("uses a factual sessions-by-date histogram in the overview", async () => {
render(<AnalysisPage />);
expect(await screen.findByText("Séances par date")).toBeInTheDocument();
expect(
screen.getByRole("img", { name: "Histogramme des séances par date" }),
).toBeInTheDocument();
expect(document.querySelector(".activity-bar")).toHaveAttribute(
"title",
expect.stringContaining("2 séance(s)"),
);
});
});