mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-18 19:31:27 +00:00
97 lines
6.9 KiB
TypeScript
97 lines
6.9 KiB
TypeScript
import { beforeEach, expect, it, vi } from "vitest";
|
||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||
import { PathsTab } from "../../../src/features/stats/PathsTab";
|
||
import { usePathStats } from "../../../src/features/stats/usePathStats";
|
||
import { EChart } from "../../../src/features/stats/EChart";
|
||
import i18n from "../../../src/i18n";
|
||
|
||
const query = { data: { since: 0, until: 3600000, receptions: 100, hashed: 60, empty: 20, trace: 10, unclassified: 10,
|
||
hashWidths: [{ bytes: 1, receptions: 20 }, { bytes: 2, receptions: 30 }, { bytes: 3, receptions: 10 }], pathLengths: [{ entries: 0, receptions: 20 }, { entries: 2, receptions: 60 }],
|
||
hourly: [{ hour: 0, receptions: 100, oneByte: 20, twoByte: 30, threeByte: 10, empty: 20, trace: 10, unclassified: 10 }] }, isPending: false, isPlaceholderData: false, isError: false, isFetching: false, refetch: vi.fn() };
|
||
vi.mock("../../../src/features/stats/usePathStats", () => ({ usePathStats: vi.fn(() => query) }));
|
||
const originalData = query.data;
|
||
vi.mock("../../../src/features/stats/EChart", () => ({ EChart: vi.fn(() => <div data-testid="chart" />) }));
|
||
beforeEach(() => { query.data = originalData; query.isPending = false; query.isPlaceholderData = false; query.isError = false; vi.clearAllMocks(); });
|
||
|
||
it("translates classification and exact tables while preserving counts and the requested window", async () => {
|
||
await act(() => i18n.changeLanguage("fr"));
|
||
render(<PathsTab range="7d" />);
|
||
expect(screen.queryByRole("heading")).not.toBeInTheDocument();
|
||
expect(usePathStats).toHaveBeenCalledWith("7d");
|
||
expect(screen.getByText("7 j")).toBeInTheDocument();
|
||
expect(screen.getByText("66.7%")).toBeInTheDocument();
|
||
const table = screen.getByRole("table", { name: "Effectifs par catégorie de chemin" });
|
||
expect(within(table).getByRole("row", { name: /Chemins hachés.*60.*60.0%/ })).toBeInTheDocument();
|
||
expect(within(table).getByRole("row", { name: /Vides.*20.*20.0%/ })).toBeInTheDocument();
|
||
expect(within(table).getByRole("row", { name: /Non classés.*10.*10.0%/ })).toBeInTheDocument();
|
||
expect(screen.getByText(/1970-01-01 00:00.*1970-01-01 01:00.*UTC/)).toHaveTextContent("heure en cours exclue");
|
||
fireEvent.click(screen.getByText("Valeurs exactes des largeurs, longueurs et par heure"));
|
||
expect(screen.getByRole("table", { name: "Effectifs par largeur de hachage" })).toHaveTextContent("Octets par hachage");
|
||
expect(within(screen.getByRole("table", { name: "Effectifs par nombre d’entrées" })).getByRole("row", { name: "0 20" })).toBeInTheDocument();
|
||
expect(within(screen.getByRole("table", { name: "Effectifs horaires des chemins" })).getByRole("row", { name: "1970-01-01 00:00 100 20 30 10 20 10 10" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("redraws all chart labels on language change and keeps exact details open", async () => {
|
||
render(<PathsTab range="24h" />);
|
||
fireEvent.click(screen.getByText("Exact width, path-length and hourly values"));
|
||
const chartOptions = () => vi.mocked(EChart).mock.calls.slice(-4).map(([{ option }]) => option);
|
||
const before = chartOptions();
|
||
expect(before).toHaveLength(4);
|
||
await act(() => i18n.changeLanguage("fr"));
|
||
expect(screen.getByText("Valeurs exactes des largeurs, longueurs et par heure").closest("details")).toHaveAttribute("open");
|
||
const after = chartOptions();
|
||
expect(after[0]).toMatchObject({ series: [{ data: [{ name: "1 octet", value: 20 }, { name: "2 octets", value: 30 }, { name: "3 octets", value: 10 }] }] });
|
||
expect(after[1]).toMatchObject({ xAxis: { name: "Entrées du chemin" }, series: [{ name: "Observations", data: [{ value: 20 }, { value: 0 }, { value: 60 }] }] });
|
||
expect(after[2]).toMatchObject({ series: [{ name: "1 octet", data: [[0, 20]] }, { name: "2 octets", data: [[0, 30]] }, { name: "3 octets", data: [[0, 10]] }] });
|
||
expect(after[3]).toMatchObject({ series: [{ data: [{ name: "Chemins hachés", value: 60 }, { name: "Vides", value: 20 }, { name: "Trace", value: 10 }, { name: "Non classés", value: 10 }] }] });
|
||
after.forEach((option, i) => expect(option.aria).not.toEqual(before[i]!.aria));
|
||
expect(query.refetch).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("uses only nonempty hash paths for multi-byte share, exposes categories and explains remaining routes", () => {
|
||
render(<PathsTab range="24h" />);
|
||
expect(usePathStats).toHaveBeenCalledWith("24h");
|
||
expect(screen.getByText("66.7%")).toBeInTheDocument();
|
||
expect(screen.getByText(/unclassified packets/i)).toHaveTextContent("no usable path data");
|
||
const table = screen.getByRole("table", { name: "Path classification counts" });
|
||
expect(within(table).getByRole("row", { name: /Hash paths.*60.*60.0%/ })).toBeInTheDocument();
|
||
expect(within(table).getByRole("row", { name: /Empty.*20.*20.0%/ })).toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: /Refresh/ })).not.toBeInTheDocument();
|
||
});
|
||
it.each(["isPending", "isPlaceholderData", "isError"] as const)("hides old values when %s", (state) => {
|
||
query[state] = true; render(<PathsTab range="7d" />);
|
||
expect(screen.queryByText("66.7%")).not.toBeInTheDocument();
|
||
expect(screen.queryAllByTestId("chart")).toHaveLength(0);
|
||
if (state === "isError") expect(screen.getByRole("alert")).toHaveTextContent("shorter");
|
||
});
|
||
|
||
it.each(["isPending", "isPlaceholderData", "isError"] as const)("translates %s without displaying old paths", async (state) => {
|
||
query[state] = true;
|
||
await act(() => i18n.changeLanguage("fr"));
|
||
render(<PathsTab range="30d" />);
|
||
expect(screen.queryByText("66.7%")).not.toBeInTheDocument();
|
||
expect(screen.queryAllByTestId("chart")).toHaveLength(0);
|
||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||
if (state === "isError") {
|
||
expect(screen.getByRole("alert")).toHaveTextContent("période plus courte");
|
||
expect(screen.getByText("Données indisponibles")).toBeInTheDocument();
|
||
} else expect(screen.getByText("Chargement des chemins…")).toBeInTheDocument();
|
||
});
|
||
|
||
it("translates empty coverage without inventing hash-width votes or hourly records", async () => {
|
||
query.data = { ...originalData, receptions: 0, hashed: 0, empty: 0, trace: 0, unclassified: 0, hashWidths: [], pathLengths: [], hourly: [] };
|
||
await act(() => i18n.changeLanguage("fr"));
|
||
render(<PathsTab range="24h" />);
|
||
expect(screen.getByText("Aucune observation dans cette période.")).toBeInTheDocument();
|
||
expect(screen.getAllByText("Aucune donnée")).toHaveLength(4);
|
||
expect(screen.queryAllByTestId("chart")).toHaveLength(0);
|
||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("draws hourly sparks for observations, hash paths, multi-byte share and most path entries", () => {
|
||
const hour = (h: number) => ({ hour: h * 3600000, receptions: 50 + h, oneByte: 10, twoByte: 5 + h, threeByte: 1, empty: 5, trace: 1, unclassified: 1, maxEntries: 3 + h });
|
||
query.data = { ...originalData, until: 3 * 3600000, hourly: [hour(0), hour(1), hour(2)] };
|
||
const { container } = render(<PathsTab range="24h" />);
|
||
expect(container.querySelectorAll("polyline")).toHaveLength(4);
|
||
});
|