test: add unit tests for various components and utilities

- Created tests for formatBytes function to ensure correct formatting of byte values.
- Implemented tests for InjectContext to validate resource injection and cleanup.
- Added tests for logo-class helpers to verify platform alias mappings.
- Developed tests for nezha-api functions to check API response handling.
- Introduced tests for static geographic data to validate country coordinates and GeoJSON structure.
- Created comprehensive tests for utility functions including date formatting and public note parsing.
- Added tests for Server page to ensure correct rendering and functionality.
- Implemented tests for simple pages including error handling and navigation.
- Set up testing environment with Vitest and configured coverage reporting.
This commit is contained in:
hamster1963
2026-06-15 11:29:48 +08:00
parent 5670811991
commit 5720af9108
38 changed files with 5686 additions and 23 deletions
+382
View File
@@ -0,0 +1,382 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SortProvider } from "@/context/sort-provider";
import { StatusProvider } from "@/context/status-provider";
import type { WebSocketContextType } from "@/context/websocket-context";
import { WebSocketContext } from "@/context/websocket-context";
import { useStatus } from "@/hooks/use-status";
import Servers from "@/pages/Server";
import { createServer } from "@/test/fixtures";
import { renderWithProviders } from "@/test/utils";
import type { NezhaServer } from "@/types/nezha-api";
const apiMocks = vi.hoisted(() => ({
fetchServerGroup: vi.fn(),
fetchService: vi.fn(),
}));
vi.mock("@/lib/nezha-api", () => apiMocks);
vi.mock("@/components/GlobalMap", () => ({
default: ({ serverList }: { serverList: NezhaServer[] }) => (
<div data-testid="global-map">{serverList.length}</div>
),
}));
vi.mock("@/components/GroupSwitch", () => ({
default: ({
tabs,
setCurrentTab,
}: {
tabs: string[];
setCurrentTab: (tab: string) => void;
}) => (
<div>
{tabs.map((tab) => (
<button
key={tab}
type="button"
data-testid={`group-${tab}`}
onClick={() => setCurrentTab(tab)}
>
{tab}
</button>
))}
</div>
),
}));
vi.mock("@/components/ServerOverview", () => ({
default: ({
offline,
online,
total,
}: {
offline: number;
online: number;
total: number;
}) => (
<div data-testid="server-overview">{`${total}:${online}:${offline}`}</div>
),
}));
vi.mock("@/components/ServerCard", () => ({
default: ({ serverInfo }: { serverInfo: NezhaServer }) => (
<article data-testid="server-card">{serverInfo.name}</article>
),
}));
vi.mock("@/components/ServerCardInline", () => ({
default: ({ serverInfo }: { serverInfo: NezhaServer }) => (
<article data-testid="server-card-inline">{serverInfo.name}</article>
),
}));
vi.mock("@/components/ServiceTracker", () => ({
ServiceTracker: ({ serverList }: { serverList: NezhaServer[] }) => (
<div data-testid="service-tracker">{serverList.length}</div>
),
}));
function StatusControl() {
const { setStatus } = useStatus();
return (
<button type="button" onClick={() => setStatus("online")}>
online-only
</button>
);
}
function renderServerPage(
websocketValue: Partial<WebSocketContextType>,
{ withStatusControl = false } = {},
) {
const defaultWebsocketValue: WebSocketContextType = {
lastMessage: null,
connected: false,
messageHistory: [],
reconnect: vi.fn(),
needReconnect: false,
setNeedReconnect: vi.fn(),
};
return renderWithProviders(
<SortProvider>
<StatusProvider>
<WebSocketContext.Provider
value={{ ...defaultWebsocketValue, ...websocketValue }}
>
{withStatusControl && <StatusControl />}
<Servers />
</WebSocketContext.Provider>
</StatusProvider>
</SortProvider>,
);
}
function websocketPayload(servers: NezhaServer[]) {
return {
data: JSON.stringify({
now: Date.parse("2025-01-01T00:00:20.000Z"),
servers,
}),
};
}
describe("Servers page", () => {
beforeEach(() => {
apiMocks.fetchServerGroup.mockResolvedValue({
success: true,
data: [
{
group: {
id: 1,
created_at: "",
updated_at: "",
name: "Edge",
},
servers: [2],
},
],
});
apiMocks.fetchService.mockResolvedValue({
success: true,
data: {
services: {},
cycle_transfer_stats: {},
},
});
});
it("renders websocket loading and processing states", () => {
const { rerender } = renderServerPage({
connected: false,
lastMessage: null,
});
expect(screen.getByText("info.websocketConnecting")).toBeInTheDocument();
rerender(
<SortProvider>
<StatusProvider>
<WebSocketContext.Provider
value={{
lastMessage: null,
connected: true,
messageHistory: [],
reconnect: vi.fn(),
needReconnect: false,
setNeedReconnect: vi.fn(),
}}
>
<Servers />
</WebSocketContext.Provider>
</StatusProvider>
</SortProvider>,
);
expect(screen.getByText("info.processing")).toBeInTheDocument();
});
it("summarizes online and offline servers from websocket data", async () => {
const online = createServer({ id: 1, name: "alpha" });
const offline = createServer({
id: 2,
name: "beta",
last_active: "2024-12-31T23:00:00.000Z",
});
renderServerPage({
connected: true,
lastMessage: websocketPayload([online, offline]),
});
expect(screen.getByTestId("server-overview")).toHaveTextContent("2:1:1");
expect(screen.getAllByTestId("server-card")).toHaveLength(2);
expect(screen.getByText("alpha")).toBeInTheDocument();
expect(screen.getByText("beta")).toBeInTheDocument();
await waitFor(() => {
expect(apiMocks.fetchServerGroup).toHaveBeenCalled();
expect(apiMocks.fetchService).toHaveBeenCalled();
});
});
it("filters servers by selected group", async () => {
const online = createServer({ id: 1, name: "alpha" });
const offline = createServer({
id: 2,
name: "beta",
last_active: "2024-12-31T23:00:00.000Z",
});
const user = userEvent.setup();
renderServerPage({
connected: true,
lastMessage: websocketPayload([online, offline]),
});
await user.click(await screen.findByTestId("group-Edge"));
expect(screen.queryByText("alpha")).not.toBeInTheDocument();
expect(screen.getByText("beta")).toBeInTheDocument();
expect(sessionStorage.getItem("selectedGroup")).toBe("Edge");
});
it("sorts server cards by selected metrics and direction", async () => {
const lowCpu = createServer({
id: 1,
name: "alpha",
state: { cpu: 10 },
});
const highCpu = createServer({
id: 2,
name: "beta",
state: { cpu: 90 },
});
const user = userEvent.setup();
renderServerPage({
connected: true,
lastMessage: websocketPayload([lowCpu, highCpu]),
});
await user.selectOptions(screen.getByLabelText("Sort metric"), "cpu");
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("beta");
await user.click(screen.getByLabelText("Toggle sort direction"));
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
});
it("keeps name sorting independent from online status", async () => {
const onlineAlpha = createServer({
id: 1,
name: "alpha",
});
const offlineZeta = createServer({
id: 2,
name: "zeta",
last_active: "2024-12-31T23:00:00.000Z",
});
const user = userEvent.setup();
renderServerPage({
connected: true,
lastMessage: websocketPayload([onlineAlpha, offlineZeta]),
});
expect(screen.getByLabelText("Toggle sort direction")).toBeDisabled();
await user.selectOptions(screen.getByLabelText("Sort metric"), "name");
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("zeta");
await user.click(screen.getByLabelText("Toggle sort direction"));
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
});
it("toggles map and service tracker controls when service data exists", async () => {
apiMocks.fetchService.mockResolvedValue({
success: true,
data: {
services: {
http: {
service_name: "HTTP",
current_up: 1,
current_down: 0,
total_up: 1,
total_down: 0,
delay: [10],
up: [1],
down: [0],
},
},
cycle_transfer_stats: {},
},
});
const user = userEvent.setup();
const online = createServer({ id: 1, name: "alpha" });
const offline = createServer({
id: 2,
name: "beta",
last_active: "2024-12-31T23:00:00.000Z",
});
const { container } = renderServerPage({
connected: true,
lastMessage: websocketPayload([online, offline]),
});
await waitFor(() => {
expect(apiMocks.fetchService).toHaveBeenCalled();
expect(
container.querySelectorAll(
".server-overview-controls section > button",
),
).toHaveLength(3);
});
const controls = container.querySelectorAll(
".server-overview-controls section > button",
);
await user.click(controls[0]);
expect(screen.getByTestId("global-map")).toHaveTextContent("2");
expect(localStorage.getItem("showMap")).toBe("1");
await user.click(controls[1]);
expect(screen.getByTestId("service-tracker")).toHaveTextContent("2");
expect(localStorage.getItem("showServices")).toBe("1");
});
it("does not enable inline cards from storage on mobile widths", () => {
localStorage.setItem("inline", "1");
Object.defineProperty(window, "innerWidth", {
configurable: true,
value: 500,
});
const online = createServer({ id: 1, name: "alpha" });
renderServerPage({
connected: true,
lastMessage: websocketPayload([online]),
});
expect(screen.getByTestId("server-card")).toHaveTextContent("alpha");
expect(screen.queryByTestId("server-card-inline")).not.toBeInTheDocument();
});
it("applies external status filters and inline card preferences", async () => {
localStorage.setItem("inline", "1");
Object.defineProperty(window, "innerWidth", {
configurable: true,
value: 1024,
});
const online = createServer({ id: 1, name: "alpha" });
const offline = createServer({
id: 2,
name: "beta",
last_active: "2024-12-31T23:00:00.000Z",
});
const user = userEvent.setup();
renderServerPage(
{
connected: true,
lastMessage: websocketPayload([online, offline]),
},
{ withStatusControl: true },
);
await waitFor(() => {
expect(screen.getAllByTestId("server-card-inline")).toHaveLength(2);
});
await user.click(screen.getByRole("button", { name: "online-only" }));
expect(screen.getAllByTestId("server-card-inline")).toHaveLength(1);
expect(screen.getByText("alpha")).toBeInTheDocument();
expect(screen.queryByText("beta")).not.toBeInTheDocument();
});
});
+130
View File
@@ -0,0 +1,130 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ErrorPage from "@/pages/ErrorPage";
import NotFound from "@/pages/NotFound";
import ServerDetail from "@/pages/ServerDetail";
vi.mock("@/components/NetworkChart", () => ({
NetworkChart: ({ server_id, show }: { server_id: number; show: boolean }) => (
<div data-testid="network-chart">{`${server_id}:${show}`}</div>
),
}));
vi.mock("@/components/ServerDetailChart", () => ({
default: ({ server_id }: { server_id: string }) => (
<div data-testid="detail-chart">{server_id}</div>
),
}));
vi.mock("@/components/ServerDetailOverview", () => ({
default: ({ server_id }: { server_id: string }) => (
<div data-testid="detail-overview">{server_id}</div>
),
}));
vi.mock("@/components/TabSwitch", () => ({
default: ({
tabs,
setCurrentTab,
}: {
tabs: string[];
setCurrentTab: (tab: string) => void;
}) => (
<div>
{tabs.map((tab) => (
<button key={tab} type="button" onClick={() => setCurrentTab(tab)}>
{tab}
</button>
))}
</div>
),
}));
function LocationProbe() {
const location = useLocation();
return <p>{location.pathname}</p>;
}
describe("simple pages", () => {
it("renders explicit and translated error messages", () => {
const { rerender } = render(<ErrorPage code={418} message="short" />);
expect(screen.getByText("418")).toBeInTheDocument();
expect(screen.getByText("short")).toBeInTheDocument();
rerender(<ErrorPage />);
expect(screen.getByText("error.somethingWentWrong")).toBeInTheDocument();
});
it("navigates back home from the not found page", async () => {
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={["/missing"]}>
<Routes>
<Route
path="/missing"
element={
<>
<NotFound />
<LocationProbe />
</>
}
/>
<Route
path="/"
element={
<>
<p>home</p>
<LocationProbe />
</>
}
/>
</Routes>
</MemoryRouter>,
);
expect(screen.getByText("404")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "error.backToHome" }));
expect(screen.getByText("home")).toBeInTheDocument();
expect(screen.getByText("/")).toBeInTheDocument();
});
});
describe("ServerDetail", () => {
beforeEach(() => {
vi.stubGlobal("scrollTo", vi.fn());
});
it("renders detail tab by default and can switch to network tab", async () => {
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={["/server/7"]}>
<Routes>
<Route path="/server/:id" element={<ServerDetail />} />
</Routes>
</MemoryRouter>,
);
expect(screen.getByTestId("detail-overview")).toHaveTextContent("7");
expect(screen.getByTestId("detail-chart")).toHaveTextContent("7");
expect(screen.getByTestId("network-chart")).toHaveTextContent("7:false");
await user.click(screen.getByRole("button", { name: "Network" }));
expect(screen.getByTestId("network-chart")).toHaveTextContent("7:true");
});
it("redirects when route params are missing", async () => {
render(
<MemoryRouter initialEntries={["/server"]}>
<Routes>
<Route path="/server" element={<ServerDetail />} />
<Route path="/404" element={<p>redirected</p>} />
</Routes>
</MemoryRouter>,
);
expect(await screen.findByText("redirected")).toBeInTheDocument();
});
});