Initial commit

This commit is contained in:
2026-05-20 00:18:07 +08:00
commit e2bf594719
58 changed files with 5885 additions and 0 deletions

54
tests/web/App.test.tsx Normal file
View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/require-await */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import { createElement, StrictMode } from "react";
import { App } from "../../src/web/app";
import { ErrorBoundary } from "../../src/web/components/ErrorBoundary";
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 0,
},
},
});
}
function renderApp() {
const queryClient = createTestQueryClient();
return render(
createElement(
StrictMode,
null,
createElement(
ErrorBoundary,
null,
createElement(QueryClientProvider, { client: queryClient }, createElement(App)),
),
),
);
}
describe("App", () => {
test("渲染 Layout 骨架和品牌名", () => {
// mock /health fetch 避免网络错误
window.fetch = (async () => {
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
}) as unknown as typeof fetch;
renderApp();
expect(screen.getByText("{{app-name}}")).not.toBeNull();
expect(screen.getByText("系统")).not.toBeNull();
expect(screen.getByText("明亮")).not.toBeNull();
expect(screen.getByText("黑暗")).not.toBeNull();
});
});

51
tests/web/test-utils.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { mock } from "bun:test";
// Note: jsdom and polyfills are now set up in tests/setup.ts
// This file only contains component-specific mocks
// Mock recharts BEFORE any component imports
void mock.module("recharts", () => ({
Area: () => null,
CartesianGrid: () => null,
Line: () => null,
LineChart: ({ children }: { children: unknown }) => children,
ResponsiveContainer: ({ children }: { children: unknown }) => children,
Tooltip: () => null,
XAxis: () => null,
YAxis: () => null,
}));
// Custom test helpers (替代 jest-dom matchers)
export const testHelpers = {
toBeInTheDocument: (element: Element | null) => {
const pass = element !== null && document.contains(element);
return {
message: () => (pass ? "Expected element not to be in document" : "Expected element to be in document"),
pass,
};
},
toHaveAttribute: (element: Element | null, attr: string, value?: string) => {
const pass = value === undefined ? (element?.hasAttribute(attr) ?? false) : element?.getAttribute(attr) === value;
return {
message: () =>
pass ? `Expected element not to have attribute "${attr}"` : `Expected element to have attribute "${attr}"`,
pass,
};
},
toHaveClass: (element: Element | null, className: string) => {
const pass = element?.classList.contains(className) ?? false;
return {
message: () =>
pass ? `Expected element not to have class "${className}"` : `Expected element to have class "${className}"`,
pass,
};
},
toHaveTextContent: (element: Element | null, text: RegExp | string) => {
const content = element?.textContent ?? "";
const pass = element !== null && (typeof text === "string" ? content.includes(text) : text.test(content));
return {
message: () => (pass ? `Expected element not to have text "${text}"` : `Expected element to have text "${text}"`),
pass,
};
},
};

View File

@@ -0,0 +1,79 @@
import { describe, expect, test } from "bun:test";
import {
formatCountdown,
formatDurationUnit,
formatRelativeTime,
isOlderThan,
subtractHours,
} from "../../../src/web/utils/time";
describe("subtractHours", () => {
test("正常扣减小时", () => {
const result = subtractHours(new Date("2025-01-15T12:00:00.000Z"), 3);
expect(result.toISOString()).toBe("2025-01-15T09:00:00.000Z");
});
test("跨天扣减", () => {
const result = subtractHours(new Date("2025-01-15T02:00:00.000Z"), 6);
expect(result.toISOString()).toBe("2025-01-14T20:00:00.000Z");
});
test("跨月扣减", () => {
const result = subtractHours(new Date("2025-03-01T01:00:00.000Z"), 2);
expect(result.toISOString()).toBe("2025-02-28T23:00:00.000Z");
});
test("扣减 0 小时返回相同时间", () => {
const result = subtractHours(new Date("2025-01-15T12:00:00.000Z"), 0);
expect(result.toISOString()).toBe("2025-01-15T12:00:00.000Z");
});
});
describe("formatRelativeTime", () => {
const now = new Date("2025-01-01T00:02:00.000Z");
test("格式化秒和分钟", () => {
expect(formatRelativeTime("2025-01-01T00:01:45.000Z", now)).toBe("15秒前");
expect(formatRelativeTime("2025-01-01T00:00:00.000Z", now)).toBe("2分钟前");
});
test("无时间返回占位", () => {
expect(formatRelativeTime(null, now)).toBe("尚无检查数据");
expect(formatRelativeTime("invalid", now)).toBe("尚无检查数据");
});
});
describe("formatDurationUnit", () => {
test("按秒、分钟、小时动态格式化", () => {
expect(formatDurationUnit(1500)).toEqual({ suffix: "秒", value: 1.5 });
expect(formatDurationUnit(120000)).toEqual({ suffix: "分钟", value: 2 });
expect(formatDurationUnit(5400000)).toEqual({ suffix: "小时", value: 1.5 });
});
test("空时长返回占位", () => {
expect(formatDurationUnit(null)).toEqual({ suffix: "", value: 0 });
});
});
describe("formatCountdown", () => {
test("格式化秒级和分钟级倒计时", () => {
expect(formatCountdown(0)).toBe("0秒");
expect(formatCountdown(59)).toBe("59秒");
expect(formatCountdown(60)).toBe("1分0秒");
expect(formatCountdown(299)).toBe("4分59秒");
});
});
describe("isOlderThan", () => {
test("判断时间是否超过阈值", () => {
const now = new Date("2025-01-01T00:02:00.000Z");
expect(isOlderThan("2025-01-01T00:00:59.000Z", 60000, now)).toBe(true);
expect(isOlderThan("2025-01-01T00:01:30.000Z", 60000, now)).toBe(false);
});
});