/**
* 单元测试:Dashboard 页面 + DbHealthCard 组件
*
* _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
*
* - 测试 4 个区块正确渲染
* - 测试跳转链接指向正确路由
* - 测试 DbHealthCard connected/disconnected/timeout 状态渲染
*/
import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { MemoryRouter } from "react-router-dom";
import React from "react";
import DbHealthCard from "../components/DbHealthCard";
import type { DbHealthItem } from "../api/dbHealth";
/* ------------------------------------------------------------------ */
/* Ant Design jsdom 兼容:polyfill window.matchMedia */
/* ------------------------------------------------------------------ */
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
/* ------------------------------------------------------------------ */
/* Mock 所有 Dashboard 依赖的 API 模块 */
/* ------------------------------------------------------------------ */
vi.mock("../api/opsPanel", () => ({
fetchSystemInfo: vi.fn().mockResolvedValue({
cpu_percent: 25,
memory_total_gb: 16,
memory_used_gb: 8,
memory_percent: 50,
disk_total_gb: 500,
disk_used_gb: 250,
disk_percent: 50,
boot_time: "2026-01-01T00:00:00",
}),
fetchServicesStatus: vi.fn().mockResolvedValue([
{
env: "prod",
label: "生产环境",
running: true,
pid: 1234,
port: 8000,
uptime_seconds: 3600,
memory_mb: 256,
cpu_percent: 10,
},
]),
fetchGitInfo: vi.fn().mockResolvedValue([
{
env: "prod",
branch: "main",
last_commit_hash: "abc1234",
last_commit_message: "test commit",
last_commit_time: "2026-01-01T00:00:00",
has_local_changes: false,
},
]),
startService: vi.fn(),
stopService: vi.fn(),
restartService: vi.fn(),
gitPull: vi.fn(),
syncDeps: vi.fn(),
}));
vi.mock("../api/dbHealth", () => ({
fetchDbHealth: vi.fn().mockResolvedValue([
{
db_name: "etl_feiqiu",
status: "connected",
active_connections: 5,
idle_connections: 10,
db_size_mb: 128.5,
slow_query_count: 2,
},
{
db_name: "test_etl_feiqiu",
status: "disconnected",
active_connections: null,
idle_connections: null,
db_size_mb: null,
slow_query_count: null,
},
]),
}));
vi.mock("../api/adminAI", () => ({
getTriggerJobs: vi.fn().mockResolvedValue({
items: [],
total: 0,
page: 1,
page_size: 50,
today_skipped_duplicates: 0,
}),
}));
// Mock AIDashboard 为简单占位,避免其内部 API 调用
vi.mock("../pages/AIDashboard", () => ({
default: () =>
AIDashboard
,
}));
/* ------------------------------------------------------------------ */
/* Dashboard 页面测试 */
/* ------------------------------------------------------------------ */
describe("Dashboard 页面 (Requirements 2.1, 2.2)", () => {
let Dashboard: React.FC;
beforeEach(async () => {
vi.clearAllMocks();
const mod = await import("../pages/Dashboard");
Dashboard = mod.default;
});
it("渲染 4 个区块:OpsPanel 子组件、DbHealthCard、AI 运行总览、AI 调度摘要", async () => {
render(
,
);
// 区块 1:OpsPanel 子组件 — 页面标题"运行状态"
expect(await screen.findByText("运行状态")).toBeInTheDocument();
// 区块 2:数据库健康监控
expect(screen.getByText("数据库健康监控")).toBeInTheDocument();
// 区块 3:AI 运行总览(mocked AIDashboard)
expect(screen.getByText("AI 运行总览")).toBeInTheDocument();
expect(screen.getByTestId("ai-dashboard-mock")).toBeInTheDocument();
// 区块 4:AI 调度摘要(Divider + Card title 各出现一次)
const summaryElements = screen.getAllByText("AI 调度摘要");
expect(summaryElements.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("今日触发数")).toBeInTheDocument();
expect(screen.getByText("今日成功率")).toBeInTheDocument();
});
it("跳转链接:ETL 状态详情、触发器详情、AI 调度详情", async () => {
render(
,
);
await screen.findByText("运行状态");
expect(screen.getByText(/ETL 状态详情/)).toBeInTheDocument();
expect(screen.getByText(/触发器详情/)).toBeInTheDocument();
// AI 调度详情出现两次(顶部按钮 + 底部链接),取第一个
const aiButtons = screen.getAllByText(/AI 调度详情/);
expect(aiButtons.length).toBeGreaterThanOrEqual(1);
});
});
/* ------------------------------------------------------------------ */
/* DbHealthCard 组件测试(纯展示组件,不需要 mock API) */
/* ------------------------------------------------------------------ */
describe("DbHealthCard — connected 状态 (Requirements 2.3, 2.4)", () => {
const connectedItems: DbHealthItem[] = [
{
db_name: "etl_feiqiu",
status: "connected",
active_connections: 5,
idle_connections: 10,
db_size_mb: 128.5,
slow_query_count: 2,
},
{
db_name: "zqyy_app",
status: "connected",
active_connections: 3,
idle_connections: 7,
db_size_mb: 256.0,
slow_query_count: 0,
},
];
it("为每个 connected 数据库渲染卡片,显示「已连接」标签", () => {
render();
expect(screen.getByText("etl_feiqiu")).toBeInTheDocument();
expect(screen.getByText("zqyy_app")).toBeInTheDocument();
const connectedTags = screen.getAllByText("已连接");
expect(connectedTags).toHaveLength(2);
});
it("展示连接池指标(活跃/空闲连接数)", () => {
render();
expect(screen.getByText(/活跃 5/)).toBeInTheDocument();
expect(screen.getByText(/空闲 10/)).toBeInTheDocument();
});
it("展示数据库大小和慢查询数量", () => {
render();
const sizeLabels = screen.getAllByText("数据库大小");
expect(sizeLabels.length).toBeGreaterThanOrEqual(1);
const slowLabels = screen.getAllByText(/慢查询/);
expect(slowLabels.length).toBeGreaterThanOrEqual(1);
});
});
describe("DbHealthCard — disconnected 状态 (Requirement 2.5)", () => {
const disconnectedItems: DbHealthItem[] = [
{
db_name: "test_etl_feiqiu",
status: "disconnected",
active_connections: null,
idle_connections: null,
db_size_mb: null,
slow_query_count: null,
},
];
it("disconnected 数据库显示「未连接」标签", () => {
render();
expect(screen.getByText("test_etl_feiqiu")).toBeInTheDocument();
expect(screen.getByText("未连接")).toBeInTheDocument();
});
it("disconnected 数据库显示无法获取指标提示", () => {
render();
expect(screen.getByText("数据库未连接,无法获取指标")).toBeInTheDocument();
});
});
describe("DbHealthCard — timeout 状态", () => {
it("超时时显示「加载超时」标签", () => {
render();
expect(screen.getByText("加载超时")).toBeInTheDocument();
});
it("超时时显示重试按钮,点击触发 onRetry", () => {
const onRetry = vi.fn();
render();
const retryBtn = screen.getByText("重试");
expect(retryBtn).toBeInTheDocument();
fireEvent.click(retryBtn);
expect(onRetry).toHaveBeenCalledOnce();
});
});
describe("DbHealthCard — mixed 状态(connected + disconnected)", () => {
const mixedItems: DbHealthItem[] = [
{
db_name: "etl_feiqiu",
status: "connected",
active_connections: 5,
idle_connections: 10,
db_size_mb: 128.5,
slow_query_count: 2,
},
{
db_name: "test_etl_feiqiu",
status: "disconnected",
active_connections: null,
idle_connections: null,
db_size_mb: null,
slow_query_count: null,
},
];
it("同时渲染 connected 和 disconnected 卡片", () => {
render();
expect(screen.getByText("已连接")).toBeInTheDocument();
expect(screen.getByText("未连接")).toBeInTheDocument();
expect(screen.getByText("etl_feiqiu")).toBeInTheDocument();
expect(screen.getByText("test_etl_feiqiu")).toBeInTheDocument();
});
});