mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-20 17:51:57 +00:00
Drop analytics refresh buttons and repeated titles (#131)
This commit is contained in:
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfo } from "./SectionInfo";
|
||||
import { getObserver, getObserverComparison } from "../../api/client";
|
||||
import { useRegion } from "../../hooks/useRegion";
|
||||
import { OBSERVER_UUID } from "../observers/observer-id";
|
||||
@@ -125,7 +126,7 @@ export function CompareObserversTab() {
|
||||
] : [];
|
||||
return (
|
||||
<div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
|
||||
<div className="flex items-center gap-2"><h2 className="text-lg font-semibold text-text-bright">{t("observerCompare.title")}</h2><InfoTip text={t("observerCompare.retainedWindow")} /></div>
|
||||
<SectionInfo text={t("observerCompare.retainedWindow")} />
|
||||
<Card title={t("observerCompare.selection")}>
|
||||
{supplied && !valid && <p role="alert" className="mb-3 text-sm text-danger">{t("observerCompare.invalidLink")}</p>}
|
||||
<ComparisonForm key={keys.map((key) => params.get(key)).join("|")} initial={selection} onCompare={compare} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfo } from "./SectionInfo";
|
||||
import { formatCount, formatUtc } from "../../lib/formatters";
|
||||
import { Card, ChartCard, StatCard } from "./cards";
|
||||
import { donutOption } from "./chartOptions";
|
||||
@@ -35,10 +36,7 @@ export function PathsTab({ range }: { range: StatsRange }) {
|
||||
const largest = data?.pathLengths.at(-1)?.entries;
|
||||
|
||||
return <div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2"><h2 className="text-lg font-semibold text-text-bright">{t("stats.tabs.paths")}</h2><InfoTip text={[t("paths.subtitle"), t("paths.measurement"), data && t("paths.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} /></div>
|
||||
<button type="button" onClick={() => void query.refetch()} disabled={query.isFetching || query.isPending} className="rounded border border-border px-3 py-1.5 text-xs text-text-normal hover:bg-bg-raised disabled:opacity-50">{t("paths.refresh")}</button>
|
||||
</div>
|
||||
<SectionInfo text={[t("paths.subtitle"), t("paths.measurement"), data && t("paths.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} />
|
||||
{query.isError && <p role="alert" className="text-sm text-danger">{t("paths.error")}</p>}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<StatCard label={t("paths.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.primary} sublabel={t(`stats.ranges.${range}`)} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfo } from "./SectionInfo";
|
||||
import { useScopes } from "./useStats";
|
||||
import { useChartColors } from "./chartTheme";
|
||||
import { Card, ChartCard, StatCard } from "./cards";
|
||||
@@ -25,7 +26,7 @@ export function ScopesTab() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
|
||||
<div className="flex items-center gap-2"><h2 className="text-lg font-semibold text-text-bright">{t("scopes.title")}</h2><InfoTip text={[t("scopes.subtitle"), t("scopes.measurement")]} /></div>
|
||||
<SectionInfo text={[t("scopes.subtitle"), t("scopes.measurement")]} />
|
||||
<label className="flex max-w-sm flex-col gap-1 text-xs text-text-muted">{t("scopes.search")}
|
||||
<input type="search" value={search} onChange={(event) => setSearch(event.target.value)} placeholder="#bc, #east…" className="rounded border border-border bg-bg-raised px-3 py-2 text-base text-text-bright outline-none focus:border-primary sm:text-sm" />
|
||||
</label>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext, type ComponentProps } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfoSlot } from "./section-info-slot";
|
||||
|
||||
// Sits beside the section chooser in the analytics bar; a tab rendered outside it keeps the tip in place.
|
||||
export function SectionInfo({ text }: ComponentProps<typeof InfoTip>) {
|
||||
const slot = useContext(SectionInfoSlot);
|
||||
return slot ? createPortal(<InfoTip text={text} />, slot) : <InfoTip text={text} />;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfo } from "./SectionInfo";
|
||||
import { formatCount, formatUtc } from "../../lib/formatters";
|
||||
import { Card, ChartCard, StatCard } from "./cards";
|
||||
import { useChartColors } from "./chartTheme";
|
||||
@@ -25,10 +26,7 @@ export function SignalTab({ range }: { range: StatsRange }) {
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2"><h2 className="text-lg font-semibold text-text-bright">{t("signal.title")}</h2><InfoTip text={[t("signal.subtitle"), t("signal.measurement"), data && t("signal.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} /></div>
|
||||
<button type="button" onClick={() => void query.refetch()} disabled={query.isFetching || query.isPending} className="rounded border border-border px-3 py-1.5 text-xs text-text-normal hover:bg-bg-raised disabled:opacity-50">{t("signal.refresh")}</button>
|
||||
</div>
|
||||
<SectionInfo text={[t("signal.subtitle"), t("signal.measurement"), data && t("signal.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} />
|
||||
{query.isError && <p role="alert" className="text-sm text-danger">{t("signal.error")}</p>}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<StatCard label={t("signal.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.primary} sublabel={t(`stats.ranges.${range}`)} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import type { WsManager } from "../../api/ws-manager";
|
||||
import { StatsSubHeader } from "./StatsSubHeader";
|
||||
import { SectionInfoSlot } from "./section-info-slot";
|
||||
import { MeshTab } from "./MeshTab";
|
||||
import { TrafficTab } from "./TrafficTab";
|
||||
import { SignalTab } from "./SignalTab";
|
||||
@@ -33,6 +34,7 @@ export function StatsOverview({ wsManager, onViewNode }: StatsOverviewProps) {
|
||||
const tab = asTab(params.get("statsTab"));
|
||||
const range = asRange(params.get("range"));
|
||||
const legacyObserver = params.get("statsTab") === "observer";
|
||||
const [infoSlot, setInfoSlot] = useState<HTMLElement | null>(null);
|
||||
|
||||
const patch = useCallback(
|
||||
(updates: Record<string, string | null>) => {
|
||||
@@ -62,7 +64,8 @@ export function StatsOverview({ wsManager, onViewNode }: StatsOverviewProps) {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<StatsSubHeader tab={tab} onTabChange={handleTab} range={range} onRangeChange={handleRange} />
|
||||
<StatsSubHeader tab={tab} onTabChange={handleTab} range={range} onRangeChange={handleRange} infoSlot={setInfoSlot} />
|
||||
<SectionInfoSlot.Provider value={infoSlot}>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
{tab === "mesh" && <MeshTab range={range} onSelectObserver={handleSelectObserver} wsManager={wsManager} />}
|
||||
{tab === "traffic" && <TrafficTab range={range} />}
|
||||
@@ -74,6 +77,7 @@ export function StatsOverview({ wsManager, onViewNode }: StatsOverviewProps) {
|
||||
{tab === "graph" && <NeighbourGraphTab />}
|
||||
{tab === "compare" && <CompareObserversTab />}
|
||||
</div>
|
||||
</SectionInfoSlot.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,15 +97,17 @@ interface Props {
|
||||
onTabChange: (tab: StatsTab) => void;
|
||||
range: StatsRange;
|
||||
onRangeChange: (range: StatsRange) => void;
|
||||
infoSlot?: (element: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
export function StatsSubHeader({ tab, onTabChange, range, onRangeChange }: Props) {
|
||||
export function StatsSubHeader({ tab, onTabChange, range, onRangeChange, infoSlot }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
const tabOptions = TAB_OPTIONS.map((option) => ({ ...option, label: t(`stats.tabs.${option.value}`) }));
|
||||
const rangeOptions = RANGES.map((value) => ({ value, label: t(`stats.ranges.${value}`) }));
|
||||
return (
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-border bg-bg-surface px-4 py-2.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{/* pills don't scale on a phone as sections grow — swap to a compact dropdown there */}
|
||||
{isMobile ? (
|
||||
<SelectDropdown
|
||||
@@ -127,6 +129,8 @@ export function StatsSubHeader({ tab, onTabChange, range, onRangeChange }: Props
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span ref={infoSlot} data-section-info className="flex shrink-0 empty:hidden" />
|
||||
</div>
|
||||
{/* Comparison has explicit dates; graph and clock drift have no rolling window. */}
|
||||
{tab !== "graph" && tab !== "clockdrift" && tab !== "compare" && tab !== "scopes" && (
|
||||
<Segmented
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoTip } from "../../components/InfoTip";
|
||||
import { SectionInfo } from "./SectionInfo";
|
||||
import { formatCount } from "../../lib/formatters";
|
||||
import { useStatsObservations } from "./useStats";
|
||||
import { tooltipStyle, useChartColors } from "./chartTheme";
|
||||
@@ -26,10 +27,7 @@ export function TrafficTab({ range }: { range: StatsRange }) {
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2"><h2 className="text-lg font-semibold text-text-bright">{t("stats.tabs.traffic")}</h2><InfoTip text={[t("traffic.subtitle"), t("traffic.measurement")]} /></div>
|
||||
<button type="button" onClick={() => void query.refetch()} disabled={query.isFetching || query.isPending} className="rounded border border-border px-3 py-1.5 text-xs text-text-normal hover:bg-bg-raised disabled:opacity-50">{t("traffic.refresh")}</button>
|
||||
</div>
|
||||
<SectionInfo text={[t("traffic.subtitle"), t("traffic.measurement")]} />
|
||||
{query.isError && <p role="alert" className="text-sm text-danger">{t("traffic.error")}</p>}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<StatCard label={t("traffic.receptions")} value={value(model.total)} accent={colors.primary} sublabel={t(`stats.ranges.${range}`)} />
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const SectionInfoSlot = createContext<HTMLElement | null>(null);
|
||||
@@ -110,9 +110,7 @@
|
||||
}
|
||||
},
|
||||
"signal": {
|
||||
"title": "RF / Signal",
|
||||
"subtitle": "Signal strength of packets heard by observers.",
|
||||
"refresh": "Refresh signal",
|
||||
"error": "Could not load signal data. Try refreshing or choosing a shorter time period.",
|
||||
"receptions": "Observations",
|
||||
"meanSnr": "Mean SNR",
|
||||
@@ -150,7 +148,6 @@
|
||||
},
|
||||
"paths": {
|
||||
"subtitle": "How received packets carry route hashes.",
|
||||
"refresh": "Refresh paths",
|
||||
"error": "Could not load path data. Try refreshing or choosing a shorter time period.",
|
||||
"receptions": "Observations",
|
||||
"withHashPaths": "With hash paths",
|
||||
@@ -196,7 +193,6 @@
|
||||
},
|
||||
"traffic": {
|
||||
"subtitle": "When and where observers hear packets.",
|
||||
"refresh": "Refresh traffic",
|
||||
"error": "Could not load traffic. Try refreshing.",
|
||||
"receptions": "Observations",
|
||||
"reportingIatas": "Active areas",
|
||||
@@ -226,7 +222,6 @@
|
||||
"heatmapTooltip_other": "{{day}} {{hour}}:00 UTC\n{{value}} observations"
|
||||
},
|
||||
"scopes": {
|
||||
"title": "Transport scopes",
|
||||
"subtitle": "Scope activity for the selected region.",
|
||||
"search": "Find a scope",
|
||||
"error": "Could not load scopes. Try refreshing.",
|
||||
|
||||
@@ -112,9 +112,7 @@
|
||||
}
|
||||
},
|
||||
"signal": {
|
||||
"title": "RF / Signal",
|
||||
"subtitle": "Force du signal des paquets reçus par les observateurs.",
|
||||
"refresh": "Actualiser le signal",
|
||||
"error": "Impossible de charger les données du signal. Réessayez ou choisissez une période plus courte.",
|
||||
"receptions": "Observations",
|
||||
"meanSnr": "SNR moyen",
|
||||
@@ -152,7 +150,6 @@
|
||||
},
|
||||
"paths": {
|
||||
"subtitle": "Les hachages de route portés par les paquets reçus.",
|
||||
"refresh": "Actualiser les chemins",
|
||||
"error": "Impossible de charger les données des chemins. Réessayez ou choisissez une période plus courte.",
|
||||
"receptions": "Observations",
|
||||
"withHashPaths": "Avec chemins hachés",
|
||||
@@ -199,7 +196,6 @@
|
||||
},
|
||||
"traffic": {
|
||||
"subtitle": "Quand et où les observateurs reçoivent des paquets.",
|
||||
"refresh": "Actualiser le trafic",
|
||||
"error": "Impossible de charger le trafic. Réessayez.",
|
||||
"receptions": "Observations",
|
||||
"reportingIatas": "Zones actives",
|
||||
@@ -230,7 +226,6 @@
|
||||
"heatmapTooltip_other": "{{day}} {{hour}}:00 UTC\n{{value}} observations"
|
||||
},
|
||||
"scopes": {
|
||||
"title": "Scopes de transport",
|
||||
"subtitle": "Activité des scopes pour la région sélectionnée.",
|
||||
"search": "Rechercher un scope",
|
||||
"error": "Impossible de charger les scopes. Réessayez.",
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("observer comparison", () => {
|
||||
mount(query);
|
||||
const table = await screen.findByRole("table", { name: "Comparaison des paquets flood" });
|
||||
expect(within(table).getByRole("row", { name: /A seulement.*1.*25.0%/ })).toBeInTheDocument();
|
||||
expect(screen.getByText("Comparer les observateurs")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Comparer les observateurs")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("A : Rooftop · B : Hilltop")).toBeInTheDocument();
|
||||
expect(screen.getByText(/4 paquets flood/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Comparer" })).toBeInTheDocument();
|
||||
|
||||
@@ -16,7 +16,7 @@ beforeEach(() => { query.data = originalData; query.isPending = false; query.isP
|
||||
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.getByRole("heading", { name: "Chemins et hachages" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading")).not.toBeInTheDocument();
|
||||
expect(usePathStats).toHaveBeenCalledWith("7d");
|
||||
expect(screen.getByText("7 j")).toBeInTheDocument();
|
||||
expect(screen.getByText("66.7%")).toBeInTheDocument();
|
||||
@@ -29,8 +29,6 @@ it("translates classification and exact tables while preserving counts and the r
|
||||
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();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Actualiser les chemins" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("redraws all chart labels on language change and keeps exact details open", async () => {
|
||||
@@ -58,8 +56,7 @@ it("uses only nonempty hash paths for multi-byte share, exposes categories and e
|
||||
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();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refresh paths" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
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" />);
|
||||
|
||||
@@ -15,7 +15,7 @@ beforeEach(() => { vi.clearAllMocks(); query.data = originalData; query.isError
|
||||
it("translates scope counts and membership guidance without changing scope names or values", async () => {
|
||||
await act(() => i18n.changeLanguage("fr"));
|
||||
render(<ScopesTab />);
|
||||
expect(screen.getByRole("heading", { name: "Scopes de transport" })).toBeInTheDocument();
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { StatsOverview } from "../../../src/features/stats/StatsOverview";
|
||||
import { SectionInfo } from "../../../src/features/stats/SectionInfo";
|
||||
import type { WsManager } from "../../../src/api/ws-manager";
|
||||
|
||||
vi.mock("../../../src/features/stats/ScopesTab", async () => {
|
||||
const { SectionInfo } = await import("../../../src/features/stats/SectionInfo");
|
||||
return { ScopesTab: () => <main><SectionInfo text="About scopes" /></main> };
|
||||
});
|
||||
|
||||
it("shows a section's info beside the section chooser", () => {
|
||||
render(<MemoryRouter initialEntries={["/?tab=Analytics&statsTab=scopes"]}><StatsOverview wsManager={{} as WsManager} /></MemoryRouter>);
|
||||
const tip = screen.getByRole("button", { name: /About scopes/ });
|
||||
expect(tip.closest("[data-section-info]")).toBeInTheDocument();
|
||||
expect(screen.getByRole("main")).not.toContainElement(tip);
|
||||
});
|
||||
|
||||
it("keeps the info in place outside the analytics bar", () => {
|
||||
render(<main><SectionInfo text="Standalone" /></main>);
|
||||
expect(screen.getByRole("main")).toContainElement(screen.getByRole("button", { name: /Standalone/ }));
|
||||
});
|
||||
@@ -15,17 +15,15 @@ beforeEach(() => { query.data = originalData; query.isPending = false; query.isP
|
||||
it("translates the signal view while retaining the requested window, true zero and exact counts", async () => {
|
||||
await act(() => i18n.changeLanguage("fr"));
|
||||
render(<SignalTab range="7d" />);
|
||||
expect(screen.getByRole("button", { name: "Actualiser le signal" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading")).not.toBeInTheDocument();
|
||||
expect(useSignalStats).toHaveBeenCalledWith("7d");
|
||||
expect(screen.getByText("0.0 dB")).toBeInTheDocument();
|
||||
const table = screen.getByRole("table", { name: "Disponibilité des échantillons du signal" });
|
||||
expect(within(table).getByRole("row", { name: /SNR.*80.*20.*80.0%/ })).toBeInTheDocument();
|
||||
expect(screen.getByText(/1970-01-01 00:00.*1970-01-01 01:00.*UTC/)).toHaveTextContent("heure en cours exclue");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Actualiser le signal" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("shows measured units, exact sample coverage and refresh without treating zero SNR as missing", () => {
|
||||
it("shows measured units and exact sample coverage without treating zero SNR as missing", () => {
|
||||
render(<SignalTab range="24h" />);
|
||||
expect(useSignalStats).toHaveBeenCalledWith("24h");
|
||||
expect(screen.getByText("0.0 dB")).toBeInTheDocument();
|
||||
@@ -34,8 +32,7 @@ it("shows measured units, exact sample coverage and refresh without treating zer
|
||||
const table = screen.getByRole("table", { name: "Signal sample availability" });
|
||||
expect(within(table).getByRole("row", { name: /SNR.*80.*20.*80.0%/ })).toBeInTheDocument();
|
||||
expect(within(table).getByRole("row", { name: /RSSI.*90.*10.*90.0%/ })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refresh signal" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
expect(screen.queryByRole("button", { name: /Refresh/ })).not.toBeInTheDocument();
|
||||
});
|
||||
it.each(["isPending", "isPlaceholderData", "isError"] as const)("hides previous filter values when %s", (state) => {
|
||||
query[state] = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { act, render, screen, within } from "@testing-library/react";
|
||||
import { TrafficTab } from "../../../src/features/stats/TrafficTab";
|
||||
import { useStatsObservations } from "../../../src/features/stats/useStats";
|
||||
import { EChart } from "../../../src/features/stats/EChart";
|
||||
@@ -19,7 +19,7 @@ describe("Traffic page", () => {
|
||||
it("translates the page and exact table without changing reception counts, IATA codes or ranges", async () => {
|
||||
await act(() => i18n.changeLanguage("fr"));
|
||||
render(<TrafficTab range="7d" />);
|
||||
expect(screen.getByRole("heading", { name: "Trafic" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading")).not.toBeInTheDocument();
|
||||
expect(useStatsObservations).toHaveBeenCalledWith("7d");
|
||||
expect(screen.getByText("Activité horaire · 7 j")).toBeInTheDocument();
|
||||
const table = screen.getByRole("table", { name: "Observations par zone" });
|
||||
@@ -29,8 +29,6 @@ describe("Traffic page", () => {
|
||||
expect(screen.getByText(/pas une panne/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /pas une panne/ })).toBeInTheDocument();
|
||||
expect(screen.queryByText(/pas une panne/, { selector: "p" })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Actualiser le trafic" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("redraws grouped and unassigned chart labels on language change without altering data", async () => {
|
||||
@@ -79,15 +77,14 @@ describe("Traffic page", () => {
|
||||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows exact reception counts, UTC/missing-history guidance and a refresh action", () => {
|
||||
it("shows exact reception counts and UTC/missing-history guidance without a refresh action", () => {
|
||||
render(<TrafficTab range="24h" />);
|
||||
expect(useStatsObservations).toHaveBeenCalledWith("24h");
|
||||
const table = screen.getByRole("table", { name: "Observations by area" });
|
||||
expect(within(table).getByRole("row", { name: /YOW.*1,200.*100.0%/ })).toBeInTheDocument();
|
||||
expect(screen.queryByText("9,999")).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/not unique packets/)).toHaveTextContent(/UTC.*outage/);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refresh traffic" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
expect(screen.queryByRole("button", { name: /Refresh/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not display previous-region data while a new query is loading", () => {
|
||||
@@ -98,12 +95,10 @@ describe("Traffic page", () => {
|
||||
expect(screen.queryAllByTestId("chart")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("provides a visible error state and retry without stale chart values", () => {
|
||||
it("provides a visible error state without stale chart values", () => {
|
||||
query.isError = true;
|
||||
render(<TrafficTab range="30d" />);
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("Could not load traffic");
|
||||
expect(screen.queryByText("YOW")).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refresh traffic" }));
|
||||
expect(query.refetch).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user