import { beforeEach, expect, it, vi } from "vitest"; import { act, fireEvent, render, screen, within } from "@testing-library/react"; import { ScopesTab } from "../../../src/features/stats/ScopesTab"; import { EChart } from "../../../src/features/stats/EChart"; import i18n from "../../../src/i18n"; const query = { data: [ { name: "#west", packetCount: 3, observerCount: 2, nodeCount: 2 }, { name: "#east", packetCount: 2, observerCount: 2, nodeCount: 3 }, ], isPending: false, isLoading: false, isPlaceholderData: false, isError: false, isFetching: false, refetch: vi.fn() }; const originalData = query.data; const H = 3_600_000; const seriesQuery = { data: { hours: [0, 1, 2].map((i) => ({ hour: i * H, status: "complete", values: null })) }, isSuccess: true, isPlaceholderData: false }; vi.mock("../../../src/features/stats/useStats", () => ({ useScopes: () => query, useStatsSeries: () => seriesQuery })); vi.mock("../../../src/features/stats/EChart", () => ({ EChart: vi.fn(() =>
) })); beforeEach(() => { vi.clearAllMocks(); query.data = originalData; query.isError = false; query.isPending = false; query.isPlaceholderData = false; }); it("translates scope counts and membership guidance without changing scope names or values", async () => { await act(() => i18n.changeLanguage("fr")); render(); expect(screen.queryByRole("heading")).not.toBeInTheDocument(); const table = screen.getByRole("table", { name: "Effectifs des scopes" }); expect(within(table).getByRole("row", { name: /#west.*3.*2.*2/ })).toBeInTheDocument(); expect(within(table).getByRole("row", { name: /#east.*2.*2.*3/ })).toBeInTheDocument(); expect(screen.getByText("Observateurs par scope")).toBeInTheDocument(); expect(screen.getByText(/plusieurs scopes/)).toHaveTextContent("couvrent la période sélectionnée"); expect(screen.getByText("2 sur 2 scopes")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Actualiser/ })).not.toBeInTheDocument(); }); it("keeps the current search and chart values when changing language", async () => { render(); fireEvent.change(screen.getByRole("searchbox", { name: "Find a scope" }), { target: { value: " EAST " } }); const charts = () => vi.mocked(EChart).mock.calls.slice(-3).map(([{ option }]) => option); const bars = (chart: ReturnType[number]) => (Array.isArray(chart.series) ? chart.series : [chart.series]).map((series) => series?.data); const before = charts(); await act(() => i18n.changeLanguage("fr")); expect(screen.getByRole("searchbox", { name: "Rechercher un scope" })).toHaveValue(" EAST "); expect(screen.getByText("1 sur 2 scopes")).toBeInTheDocument(); expect(screen.queryByText("#west")).not.toBeInTheDocument(); expect(screen.getByRole("row", { name: /#east.*2.*2.*3/ })).toBeInTheDocument(); charts().forEach((chart, i) => { expect(bars(chart)).toEqual(bars(before[i]!)); expect(chart.yAxis).toEqual(before[i]!.yAxis); expect(chart.aria).not.toEqual(before[i]!.aria); }); expect(query.refetch).not.toHaveBeenCalled(); }); it.each(["isPending", "isPlaceholderData", "isError"] as const)("translates %s and hides stale regional scopes", async (state) => { query[state] = true; await act(() => i18n.changeLanguage("fr")); render(); expect(screen.queryByRole("table")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("chart")).toHaveLength(0); expect(screen.queryByText("#west")).not.toBeInTheDocument(); if (state === "isError") { expect(screen.getByRole("alert")).toHaveTextContent("Impossible de charger les scopes"); expect(screen.getByText("Données indisponibles")).toBeInTheDocument(); } else expect(screen.getByText("Chargement des scopes…")).toBeInTheDocument(); }); it("distinguishes a translated empty dataset from a search with no match", async () => { await act(() => i18n.changeLanguage("fr")); const { rerender } = render(); const search = screen.getByRole("searchbox", { name: "Rechercher un scope" }); fireEvent.change(search, { target: { value: "#absent" } }); expect(screen.getByText("Aucun scope ne correspond à cette recherche.")).toBeInTheDocument(); expect(screen.getByText("0 sur 2 scopes")).toBeInTheDocument(); expect(screen.queryByRole("table")).not.toBeInTheDocument(); fireEvent.change(search, { target: { value: "" } }); query.data = []; rerender(); expect(screen.getByText("Aucune donnée de scope disponible.")).toBeInTheDocument(); expect(screen.getAllByText("Aucune donnée")).toHaveLength(3); expect(screen.queryAllByTestId("chart")).toHaveLength(0); }); it("shows exact scope counts, keeps membership semantics explicit and filters the whole view", () => { render(); const table = screen.getByRole("table", { name: "Scope counts" }); expect(within(table).getByRole("row", { name: /#west.*3.*2.*2/ })).toBeInTheDocument(); expect(screen.getByText("Observers by scope")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /an observer can belong/ })).toBeInTheDocument(); expect(screen.queryByText(/an observer can belong/, { selector: "p" })).not.toBeInTheDocument(); expect(screen.getByText(/Scope activity/)).toBeInTheDocument(); fireEvent.change(screen.getByRole("searchbox", { name: "Find a scope" }), { target: { value: "WEST" } }); expect(screen.queryByText("#east")).not.toBeInTheDocument(); expect(screen.getByText(/1 of 2 scopes/)).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Refresh/ })).not.toBeInTheDocument(); }); it("hides stale values while a region change is pending and on errors", () => { query.isPlaceholderData = true; const { rerender } = render(); expect(screen.queryByRole("table")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("chart")).toHaveLength(0); query.isPlaceholderData = false; query.isError = true; rerender(); expect(screen.getByRole("alert")).toHaveTextContent("Could not load scopes"); expect(screen.queryByText("#west")).not.toBeInTheDocument(); }); it("draws hourly sparks on all four scope cards from the scopes left after searching", () => { query.data = [ { name: "#west", packetCount: 3, observerCount: 2, nodeCount: 2, hourly: [{ hour: 0, packets: 1, observers: 1, nodes: 1 }, { hour: 2 * H, packets: 2, observers: 2, nodes: 0 }] }, { name: "#east", packetCount: 2, observerCount: 2, nodeCount: 3, hourly: [{ hour: H, packets: 2, observers: 1, nodes: 3 }] }, ] as typeof originalData; const { container } = render(); const points = () => [...container.querySelectorAll("polyline")].map((l) => l.getAttribute("points")); expect(points()).toHaveLength(4); expect(screen.getByText("Line: hearing per hour")).toBeInTheDocument(); expect(screen.getByText("Line: advertising per hour")).toBeInTheDocument(); const before = points(); fireEvent.change(screen.getByRole("searchbox", { name: "Find a scope" }), { target: { value: "east" } }); expect(points()).toHaveLength(4); expect(points()).not.toEqual(before); }); it("shows no scoped packet total before any hour is rolled, keeping the membership counts", async () => { await act(() => i18n.changeLanguage("en")); const saved = seriesQuery.data; seriesQuery.data = { ...saved, completeHours: 0 } as typeof saved; try { render(); expect(within(screen.getByText("Scoped packets").parentElement!.parentElement!).getByText("—")).toBeInTheDocument(); expect(within(screen.getAllByText("Observers")[0]!.parentElement!.parentElement!).getByText("4")).toBeInTheDocument(); } finally { seriesQuery.data = saved; } });