1
0
Files
DiAL/tests/web/components/App.test.tsx

233 lines
7.1 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import "../../../tests/web/test-utils";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
import { App } from "../../../src/web/app";
import { THEME_MEDIA_QUERY, THEME_PREFERENCE_STORAGE_KEY } from "../../../src/web/hooks/use-theme-preference";
function createDashboardResult(overrides = {}) {
return {
data: {
summary: {
down: 0,
incidents: 0,
lastCheckTime: "2025-01-15T10:00:00.000Z",
total: 0,
up: 0,
window: {
from: "2025-01-14T10:00:00.000Z",
label: "24h",
to: "2025-01-15T10:00:00.000Z",
},
},
targets: [],
},
dataUpdatedAt: Date.now(),
error: null,
isFetching: false,
isLoading: false,
refetch: vi.fn(),
...overrides,
};
}
function installMatchMedia(initialMatches: boolean) {
const originalMatchMedia = window.matchMedia;
let matches = initialMatches;
const listeners = new Set<(event: MediaQueryListEvent) => void>();
const mediaQueryList = {
addEventListener: (_type: string, listener: (event: MediaQueryListEvent) => void) => listeners.add(listener),
addListener: (listener: (event: MediaQueryListEvent) => void) => listeners.add(listener),
dispatchEvent: () => true,
get matches() {
return matches;
},
media: THEME_MEDIA_QUERY,
onchange: null,
removeEventListener: (_type: string, listener: (event: MediaQueryListEvent) => void) => listeners.delete(listener),
removeListener: (listener: (event: MediaQueryListEvent) => void) => listeners.delete(listener),
} as MediaQueryList;
window.matchMedia = () => mediaQueryList;
return {
restore: () => {
window.matchMedia = originalMatchMedia;
},
setMatches: (nextMatches: boolean) => {
matches = nextMatches;
listeners.forEach((listener) => listener({ matches, media: THEME_MEDIA_QUERY } as MediaQueryListEvent));
},
};
}
// Mock hooks
void vi.mock("../../../src/web/hooks/use-queries", () => ({
useDashboard: vi.fn(() => createDashboardResult()),
useMeta: vi.fn(() => ({
data: { checkerTypes: ["http", "cmd"], version: "0.1.0" },
})),
}));
void vi.mock("../../../src/web/hooks/use-target-detail", () => ({
useTargetDetail: vi.fn(() => ({
activeTab: "overview",
closeDrawer: vi.fn(),
handlePageChange: vi.fn(),
handleTabChange: vi.fn(),
handleTimeChange: vi.fn(),
historyData: {
items: [],
page: 1,
pageSize: 20,
total: 0,
},
historyLoading: false,
metricsData: null,
metricsLoading: false,
openDrawer: vi.fn(),
selectedTarget: null,
timeFrom: "",
timeTo: "",
})),
}));
describe("App", () => {
let matchMediaController: ReturnType<typeof installMatchMedia>;
beforeEach(() => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(createDashboardResult());
window.localStorage.clear();
document.documentElement.removeAttribute("theme-mode");
matchMediaController = installMatchMedia(false);
});
afterEach(() => {
matchMediaController?.restore();
});
test("渲染不崩溃", () => {
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("loading 状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
createDashboardResult({
data: null,
dataUpdatedAt: 0,
error: null,
isFetching: true,
isLoading: true,
refetch: vi.fn(),
}),
);
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("错误状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
createDashboardResult({
data: null,
dataUpdatedAt: 0,
error: { message: "Network error" },
isFetching: false,
isLoading: false,
refetch: vi.fn(),
}),
);
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("有数据状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue(
createDashboardResult({
data: {
summary: {
down: 1,
incidents: 0,
lastCheckTime: "2025-01-15T10:00:00.000Z",
total: 2,
up: 1,
window: {
from: "2025-01-14T10:00:00.000Z",
label: "24h",
to: "2025-01-15T10:00:00.000Z",
},
},
targets: [],
},
dataUpdatedAt: Date.now(),
error: null,
isFetching: false,
isLoading: false,
refetch: vi.fn(),
}),
);
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("默认渲染主题模式选项并按系统亮色应用主题", async () => {
render(<App />);
expect(screen.getByText("系统")).not.toBeNull();
expect(screen.getByText("明亮")).not.toBeNull();
expect(screen.getByText("黑暗")).not.toBeNull();
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("light"));
});
test("切换黑暗模式后写入本地存储并应用主题", async () => {
render(<App />);
fireEvent.click(screen.getByText("黑暗"));
expect(window.localStorage.getItem(THEME_PREFERENCE_STORAGE_KEY)).toBe("dark");
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark"));
});
test("刷新后恢复已保存的主题偏好", async () => {
window.localStorage.setItem(THEME_PREFERENCE_STORAGE_KEY, "dark");
render(<App />);
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark"));
});
test("系统模式响应 matchMedia 变化", async () => {
render(<App />);
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("light"));
act(() => matchMediaController.setMatches(true));
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark"));
});
test("Header 展示版本号", () => {
render(<App />);
expect(screen.getByText("v0.1.0")).not.toBeNull();
});
test("缺失版本时不展示版本占位", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries");
useMeta.mockReturnValue({
data: { checkerTypes: ["http", "cmd"] },
});
render(<App />);
expect(screen.queryByText(/v\d+\.\d+\.\d+/)).toBeNull();
});
test("复用 useMeta 查询结果", () => {
const { useMeta } = require("../../../src/web/hooks/use-queries");
render(<App />);
expect(useMeta).toHaveBeenCalled();
});
});