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();
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();
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();
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();
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();
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();
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)"),
);
});
});