Initial commit
This commit is contained in:
54
tests/web/App.test.tsx
Normal file
54
tests/web/App.test.tsx
Normal 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
51
tests/web/test-utils.tsx
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
79
tests/web/utils/time.test.ts
Normal file
79
tests/web/utils/time.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user