/* 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"] }, })), })); 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; 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(); 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(); 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(); 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(); expect(container.firstChild).not.toBeNull(); }); test("默认渲染主题模式选项并按系统亮色应用主题", async () => { render(); 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(); 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(); await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark")); }); test("系统模式响应 matchMedia 变化", async () => { render(); await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("light")); act(() => matchMediaController.setMatches(true)); await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark")); }); });