Files
nezha-dash-v1/src/test/components/header.test.tsx
T
hamster1963 5720af9108 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.
2026-06-15 11:29:48 +08:00

244 lines
6.9 KiB
TypeScript

import { QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactElement } from "react";
import { MemoryRouter, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import Header, { RefreshToast } from "@/components/Header";
import { ThemeProvider } from "@/components/ThemeProvider";
import { CommandProvider } from "@/context/command-provider";
import { createSettingResponse } from "@/test/fixtures";
import { createTestQueryClient } from "@/test/utils";
const headerMocks = vi.hoisted(() => ({
backgroundImage: undefined as string | undefined,
connected: true,
fetchLoginUser: vi.fn(),
fetchSetting: vi.fn(),
lastMessage: null as MessageEvent<string> | null,
needReconnect: false,
setNeedReconnect: vi.fn(),
updateBackground: vi.fn(),
}));
vi.mock("@numeric-text/react", () => ({
default: ({ value }: { value: string | number }) => (
<span data-testid="numeric-text">{value}</span>
),
}));
vi.mock("@/hooks/use-background", () => ({
useBackground: () => ({
backgroundImage: headerMocks.backgroundImage,
updateBackground: headerMocks.updateBackground,
}),
}));
vi.mock("@/hooks/use-websocket-context", () => ({
useWebSocketContext: () => ({
connected: headerMocks.connected,
lastMessage: headerMocks.lastMessage,
needReconnect: headerMocks.needReconnect,
setNeedReconnect: headerMocks.setNeedReconnect,
}),
}));
vi.mock("@/lib/nezha-api", () => ({
fetchLoginUser: headerMocks.fetchLoginUser,
fetchSetting: headerMocks.fetchSetting,
}));
function settingResponse(siteName = "Nezha") {
return {
...createSettingResponse(),
data: {
...createSettingResponse().data,
config: {
...createSettingResponse().data.config,
site_name: siteName,
},
},
};
}
function loginResponse() {
return {
success: true,
data: {
id: 1,
username: "admin",
password: "",
created_at: "2025-01-01T00:00:00.000Z",
updated_at: "2025-01-01T00:00:00.000Z",
},
};
}
function LocationProbe() {
const location = useLocation();
return <p>{location.pathname}</p>;
}
function renderHeader(route = "/server/1") {
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[route]}>
<ThemeProvider storageKey="header-theme-test">
<CommandProvider>
<Header />
<LocationProbe />
</CommandProvider>
</ThemeProvider>
</MemoryRouter>
</QueryClientProvider>,
);
}
function renderInRouter(ui: ReactElement) {
return render(<MemoryRouter>{ui}</MemoryRouter>);
}
describe("Header", () => {
beforeEach(() => {
headerMocks.backgroundImage = undefined;
headerMocks.connected = true;
headerMocks.fetchLoginUser.mockReset();
headerMocks.fetchSetting.mockReset();
headerMocks.lastMessage = new MessageEvent("message", {
data: JSON.stringify({ online: 4 }),
});
headerMocks.needReconnect = false;
headerMocks.setNeedReconnect.mockReset();
headerMocks.updateBackground.mockReset();
Object.assign(window, {
CustomBackgroundImage: "",
CustomDesc: "",
CustomLinks: "",
CustomLogo: "",
CustomMobileBackgroundImage: "",
});
headerMocks.fetchSetting.mockResolvedValue(settingResponse());
headerMocks.fetchLoginUser.mockRejectedValue(new Error("anonymous"));
});
it("renders configured site identity, custom links, online count, and dashboard state", async () => {
const user = userEvent.setup();
Object.assign(window, {
CustomDesc: "edge status",
CustomLinks: JSON.stringify([
{ link: "https://example.test", name: "Docs" },
]),
CustomLogo: "/logo.png",
});
Object.defineProperty(document, "cookie", {
configurable: true,
value: "session=1",
});
sessionStorage.setItem("selectedGroup", "Edge");
headerMocks.fetchSetting.mockResolvedValue(settingResponse("Status Hub"));
headerMocks.fetchLoginUser.mockResolvedValue(loginResponse());
renderHeader();
const siteName = await screen.findByText("Status Hub");
expect(screen.getByText("edge status")).toBeInTheDocument();
expect(screen.getByAltText("apple-touch-icon")).toHaveAttribute(
"src",
"/logo.png",
);
expect(screen.getAllByRole("link", { name: "Docs" })).toHaveLength(2);
expect(await screen.findAllByText("dashboard")).toHaveLength(2);
expect(screen.getByTestId("numeric-text")).toHaveTextContent("4");
expect(screen.getByText("online")).toBeInTheDocument();
await waitFor(() => {
expect(document.title).toBe("Status Hub");
});
expect(document.querySelector("link[rel='shortcut icon']")).toHaveAttribute(
"href",
"/logo.png",
);
await user.click(siteName);
expect(sessionStorage.getItem("selectedGroup")).toBeNull();
expect(screen.getByText("/")).toBeInTheDocument();
});
it("uses the offline display and login links when websocket and auth are unavailable", async () => {
headerMocks.connected = false;
const { container } = renderHeader();
expect(await screen.findAllByText("login")).toHaveLength(2);
expect(screen.getByText("offline")).toBeInTheDocument();
expect(
container.querySelector("[data-visible='true']"),
).toBeInTheDocument();
});
it("stores and removes the active custom background", async () => {
const user = userEvent.setup();
headerMocks.backgroundImage = "/desktop.png";
Object.assign(window, {
CustomBackgroundImage: "/desktop.png",
CustomMobileBackgroundImage: "/mobile.png",
});
const { container } = renderHeader();
await screen.findByText("Nezha");
const toggleButton = container
.querySelector(".lucide-image-minus")
?.closest("button");
expect(toggleButton).toBeInTheDocument();
await user.click(toggleButton as HTMLButtonElement);
expect(sessionStorage.getItem("savedBackgroundImage")).toBe("/desktop.png");
expect(headerMocks.updateBackground).toHaveBeenCalledWith(undefined);
});
it("restores the saved custom background", async () => {
const user = userEvent.setup();
sessionStorage.setItem("savedBackgroundImage", "/saved.png");
const { container } = renderHeader();
await screen.findByText("Nezha");
const toggleButton = container
.querySelector(".lucide-image-minus")
?.closest("button");
expect(toggleButton).toBeInTheDocument();
await user.click(toggleButton as HTMLButtonElement);
expect(headerMocks.updateBackground).toHaveBeenCalledWith("/saved.png");
});
});
describe("RefreshToast", () => {
beforeEach(() => {
headerMocks.needReconnect = false;
});
it("renders only while reconnect refresh is needed", () => {
vi.useFakeTimers();
sessionStorage.setItem("needRefresh", "true");
const { container, rerender } = renderInRouter(<RefreshToast />);
expect(container).toBeEmptyDOMElement();
headerMocks.needReconnect = true;
rerender(
<MemoryRouter>
<RefreshToast />
</MemoryRouter>,
);
expect(screen.getByText("refreshing...")).toBeInTheDocument();
expect(sessionStorage.getItem("needRefresh")).toBeNull();
});
});