refactor: 优化组件并添加测试覆盖

- 创建 CLAUDE.md 项目开发指导文档
- 提取公共常量到 components/constants.ts
- 修复 TimestampToDatetime 重复插件扩展问题
- 修复 DatetimeToTimestamp 时区解析问题
- 优化 RoutePersistence 移除不必要依赖
- 添加 Vitest 测试框架配置
- 为所有组件编写单元测试 (16个测试用例)
- 更新 .gitignore 忽略测试结果目录

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
雨霖铃
2026-03-16 22:10:02 +08:00
parent c9f8fe4d29
commit 9ff6b8985c
18 changed files with 2208 additions and 130 deletions
+3 -27
View File
@@ -12,32 +12,7 @@ import {
Box,
SelectChangeEvent,
} from '@mui/material';
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
const TIMESTAMP_UNITS = [
{ value: 'milliseconds', label: '毫秒 (ms)' },
{ value: 'seconds', label: '秒 (s)' },
];
import { TIME_ZONE_LIST, TIMESTAMP_UNITS } from './constants';
export function DatetimeToTimestamp() {
const [dateValue, setDateValue] = useState(() => dayjs().format('YYYY/MM/DD HH:mm:ss'));
@@ -52,7 +27,8 @@ export function DatetimeToTimestamp() {
setError('请输入有效的日期时间');
return '';
}
const timestamp = dayjs.tz(currentDate, zone);
// 使用 tz 方法直接解析带时区的日期
const timestamp = dayjs.tz(currentDate, 'YYYY/MM/DD HH:mm:ss', zone);
if (!timestamp.isValid()) {
setError('无效的日期时间格式');
return '';
+3 -5
View File
@@ -17,7 +17,6 @@ const RoutePersistence = () => {
if (lastRoute && lastRoute !== '/' && location.pathname === '/') {
navigate(lastRoute, { replace: true });
console.log('跳转路由', lastRoute);
}
} catch (err) {
console.error('恢复路由失败', err);
@@ -26,17 +25,16 @@ const RoutePersistence = () => {
}
};
restoreRoute().then(() => console.info('恢复路由成功'));
}, [location, navigate]);
restoreRoute();
}, [navigate, location.pathname]);
useEffect(() => {
const saveRoute = async () => {
if (!isRestored.current) return;
await storageUtil.set('app/lastRoute', location.pathname);
console.log('保存路由', location.pathname);
};
saveRoute().then(() => console.info('保存路由成功'));
saveRoute();
}, [location]);
return null;
+2 -32
View File
@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
import dayjs from 'dayjs';
import dayjs from '@/utils/dayjs';
import {
Button,
TextField,
@@ -12,37 +12,7 @@ import {
Box,
SelectChangeEvent,
} from '@mui/material';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
const TIMESTAMP_UNITS = [
{ value: 'milliseconds', label: '毫秒 (ms)' },
{ value: 'seconds', label: '秒 (s)' },
];
import { TIME_ZONE_LIST, TIMESTAMP_UNITS } from './constants';
export function TimestampToDatetime() {
const [timestampValue, setTimestampValue] = useState(() => dayjs().valueOf().toString());
+52
View File
@@ -0,0 +1,52 @@
import { render, screen, fireEvent } from '@testing-library/react';
import CopyButton from '../CopyButton';
import { vi } from 'vitest';
describe('CopyButton', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});
it('应该正确渲染默认文本', () => {
render(<CopyButton textToCopy="test" />);
expect(screen.getByText('复制')).toBeInTheDocument();
});
it('应该正确渲染自定义按钮文本', () => {
render(<CopyButton textToCopy="test" buttonText="Custom Copy" />);
expect(screen.getByText('Custom Copy')).toBeInTheDocument();
});
it('点击按钮时应该复制文本到剪贴板', async () => {
const writeTextMock = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: writeTextMock },
writable: true,
});
render(<CopyButton textToCopy="test content" />);
const button = screen.getByText('复制');
fireEvent.click(button);
vi.advanceTimersByTime(300);
expect(writeTextMock).toHaveBeenCalledWith('test content');
});
it('当没有提供要复制的文本时,应该在控制台警告', async () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
render(<CopyButton textToCopy="" />);
const button = screen.getByText('复制');
fireEvent.click(button);
expect(consoleWarnSpy).toHaveBeenCalledWith('没有提供要复制的文本');
consoleWarnSpy.mockRestore();
});
});
@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react';
import { DatetimeToTimestamp } from '../DatetimeToTimestamp';
describe('DatetimeToTimestamp', () => {
it('应该正确渲染组件', () => {
render(<DatetimeToTimestamp />);
expect(screen.getByLabelText('输入日期时间')).toBeInTheDocument();
expect(screen.getByText('转换')).toBeInTheDocument();
expect(screen.getByLabelText('转换结果')).toBeInTheDocument();
});
it('应该初始化为当前日期时间', () => {
render(<DatetimeToTimestamp />);
const input = screen.getByLabelText('输入日期时间') as HTMLInputElement;
expect(input.value).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
});
});
+47
View File
@@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import Navbar from '../Navbar';
import { vi } from 'vitest';
const mockItems = [
{ path: '/', label: '首页', element: <div></div> },
{ path: '/timestamp', label: '时间戳', element: <div></div> },
];
const renderWithRouter = (initialPath = '/', items = mockItems) => {
return render(
<MemoryRouter initialEntries={[initialPath]}>
<Navbar items={items} />
</MemoryRouter>,
);
};
describe('Navbar', () => {
beforeEach(() => {
// 模拟 useMediaQuery 以确保大屏幕行为
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: query === '(min-width:768px)',
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
});
});
it('应该正确渲染所有导航项(大屏幕)', () => {
renderWithRouter();
mockItems.forEach((item) => {
expect(screen.getByText(item.label)).toBeInTheDocument();
});
});
it('没有提供 items 时应该正常渲染', () => {
render(
<MemoryRouter>
<Navbar />
</MemoryRouter>,
);
expect(true).toBe(true);
});
});
@@ -0,0 +1,40 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import RoutePersistence from '../RoutePersistence';
import { storageUtil } from '@/utils/chromeStorage';
import { vi } from 'vitest';
vi.mock('@/utils/chromeStorage', () => ({
storageUtil: {
get: vi.fn(),
set: vi.fn(),
},
}));
const renderComponentOnly = (initialPath = '/') => {
return render(
<MemoryRouter initialEntries={[initialPath]}>
<RoutePersistence />
</MemoryRouter>,
);
};
describe('RoutePersistence', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('组件应该正常渲染且返回 null', () => {
const { container } = renderComponentOnly();
expect(container.firstChild).toBeNull();
});
it('当在根路由时应该从存储中恢复路由', async () => {
const mockGet = storageUtil.get as vi.Mock;
mockGet.mockResolvedValue('/timestamp');
await renderComponentOnly('/');
expect(mockGet).toHaveBeenCalledWith('app/lastRoute');
});
});
@@ -0,0 +1,39 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { TimestampExecution } from '../TimestampExecution';
import { vi } from 'vitest';
describe('TimestampExecution', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('应该正确渲染组件', () => {
render(<TimestampExecution />);
expect(screen.getByText('切换单位')).toBeInTheDocument();
expect(screen.getByText('复制')).toBeInTheDocument();
expect(screen.getByText('停止')).toBeInTheDocument();
});
it('初始状态应该显示毫秒单位', () => {
render(<TimestampExecution />);
expect(screen.getByText('毫秒')).toBeInTheDocument();
});
it('点击切换单位按钮应该切换为秒显示', () => {
render(<TimestampExecution />);
const toggleUnitButton = screen.getByText('切换单位');
fireEvent.click(toggleUnitButton);
expect(screen.getByText('秒')).toBeInTheDocument();
});
it('点击停止按钮应该停止时间戳自动更新', () => {
render(<TimestampExecution />);
const toggleButton = screen.getByText('停止');
fireEvent.click(toggleButton);
expect(screen.getByText('开始')).toBeInTheDocument();
});
});
@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react';
import { TimestampToDatetime } from '../TimestampToDatetime';
describe('TimestampToDatetime', () => {
it('应该正确渲染组件', () => {
render(<TimestampToDatetime />);
expect(screen.getByLabelText('输入时间戳')).toBeInTheDocument();
expect(screen.getByText('转换')).toBeInTheDocument();
expect(screen.getByLabelText('转换结果')).toBeInTheDocument();
});
it('应该初始化为当前时间戳', () => {
render(<TimestampToDatetime />);
const input = screen.getByLabelText('输入时间戳') as HTMLInputElement;
expect(input.value).toMatch(/^\d{13}$/);
});
});
+28
View File
@@ -0,0 +1,28 @@
export const TIME_ZONE_LIST = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
] as const;
export const TIMESTAMP_UNITS = [
{ value: 'milliseconds', label: '毫秒 (ms)' },
{ value: 'seconds', label: '秒 (s)' },
] as const;
export type TimeZone = (typeof TIME_ZONE_LIST)[number];
export type TimestampUnit = (typeof TIMESTAMP_UNITS)[number]['value'];