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:
@@ -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 '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
@@ -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}$/);
|
||||
});
|
||||
});
|
||||
@@ -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'];
|
||||
Reference in New Issue
Block a user