233 lines
7.1 KiB
TypeScript
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();
|
|
});
|
|
});
|