mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-21 02:30:15 +00:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user