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,6 +12,7 @@ node_modules
|
|||||||
stats.html
|
stats.html
|
||||||
stats-*.json
|
stats-*.json
|
||||||
.wxt
|
.wxt
|
||||||
|
.vitest
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
```
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
这是一个基于 WXT 框架的浏览器扩展项目,提供测试工具功能,包括时间戳转换、用户操作录制与回放等。
|
||||||
|
|
||||||
|
## 核心命令
|
||||||
|
|
||||||
|
### 开发相关
|
||||||
|
- `npm run dev` - 启动 Chrome 浏览器的开发模式
|
||||||
|
- `npm run dev:firefox` - 启动 Firefox 浏览器的开发模式
|
||||||
|
- `npm run build` - 构建 Chrome 浏览器的生产版本
|
||||||
|
- `npm run build:firefox` - 构建 Firefox 浏览器的生产版本
|
||||||
|
- `npm run zip` - 打包 Chrome 扩展
|
||||||
|
- `npm run zip:firefox` - 打包 Firefox 扩展
|
||||||
|
- `npm run compile` - TypeScript 类型检查(不生成文件)
|
||||||
|
- `npm run lint` - 运行 ESLint 检查
|
||||||
|
|
||||||
|
### 依赖与准备
|
||||||
|
- `npm install` - 安装依赖
|
||||||
|
- `postinstall` 会自动运行 `wxt prepare` 准备开发环境
|
||||||
|
- `prepare` 钩子会初始化 Husky Git 钩子
|
||||||
|
|
||||||
|
## 项目架构
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **框架**: WXT (Web Extension Toolkit) - 浏览器扩展开发框架
|
||||||
|
- **前端**: React 19 + TypeScript
|
||||||
|
- **UI 库**: Material UI (MUI)
|
||||||
|
- **状态管理**: React Hooks
|
||||||
|
- **数据库**: Dexie.js (IndexedDB)
|
||||||
|
- **录制回放**: rrweb
|
||||||
|
- **路由**: React Router DOM
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
```
|
||||||
|
|
||||||
|
├── components/ # 可复用 UI 组件
|
||||||
|
│ ├── CopyButton.tsx # 复制按钮组件
|
||||||
|
│ ├── DatetimeToTimestamp.tsx # 日期转时间戳组件
|
||||||
|
│ ├── Navbar.tsx # 导航栏组件
|
||||||
|
│ ├── RoutePersistence.tsx # 路由持久化组件
|
||||||
|
│ ├── TimestampExecution.tsx # 时间戳执行组件
|
||||||
|
│ └── TimestampToDatetime.tsx # 时间戳转日期组件
|
||||||
|
├── entrypoints/ # 浏览器扩展入口点
|
||||||
|
│ ├── background.ts # 后台脚本(主进程)
|
||||||
|
│ ├── content.ts # 内容脚本(注入到页面)
|
||||||
|
│ ├── offscreen/ # 离屏文档(用于长时间运行任务)
|
||||||
|
│ ├── popup/ # 扩展弹窗界面
|
||||||
|
│ │ ├── App.tsx # 弹窗主应用
|
||||||
|
│ │ ├── main.tsx # 弹窗入口
|
||||||
|
│ │ └── pages/ # 弹窗页面
|
||||||
|
│ │ ├── RecordeReplayPage.tsx # 录制回放页面
|
||||||
|
│ │ ├── TestPage.tsx # 测试页面
|
||||||
|
│ │ └── TimestampPage.tsx # 时间戳工具页面
|
||||||
|
│ └── options/ # 选项页面(未列出)
|
||||||
|
├── assets/ # 静态资源
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
│ ├── chromeStorage.ts # Chrome 存储工具
|
||||||
|
│ ├── dayjs.ts # 日期处理工具
|
||||||
|
│ ├── messages.tsx # 消息通信工具
|
||||||
|
│ ├── recordEventsDb.ts # IndexedDB 数据库工具(录制事件存储)
|
||||||
|
│ ├── recordUtils.tsx # 录制工具函数
|
||||||
|
│ ├── tabUtils.ts # 标签页工具
|
||||||
|
│ └── useRecorder.tsx # 录制器 Hook
|
||||||
|
├── types/ # 类型定义
|
||||||
|
│ └── storage.d.ts # 存储相关类型
|
||||||
|
├── public/ # 公共资源
|
||||||
|
├── package.json # 项目依赖和脚本
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
|
├── wxt.config.ts # WXT 配置
|
||||||
|
└── web-ext.config.ts # WebExtensions 配置
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
### 核心功能实现
|
||||||
|
|
||||||
|
#### 1. 时间戳转换工具
|
||||||
|
- 位置: `components/` 目录下的时间戳相关组件
|
||||||
|
- 依赖: dayjs 库进行日期处理
|
||||||
|
- 功能: 支持日期与时间戳的双向转换,支持多种格式
|
||||||
|
|
||||||
|
#### 2. 录制与回放功能
|
||||||
|
- 位置: `utils/useRecorder.tsx` (核心录制逻辑)、`utils/recordUtils.tsx` (工具函数)
|
||||||
|
- 依赖: rrweb 库
|
||||||
|
- 存储: IndexedDB (Dexie.js) - `utils/recordEventsDb.ts`
|
||||||
|
- 特点: 支持分块存储录制事件,优化性能
|
||||||
|
|
||||||
|
#### 3. 通信系统
|
||||||
|
- 位置: `utils/messages.tsx`
|
||||||
|
- 机制: 使用 `@webext-core/messaging` 库实现
|
||||||
|
- 通信通道: 后台脚本 ↔ 内容脚本 ↔ 弹窗 ↔ 离屏文档
|
||||||
|
|
||||||
|
#### 4. 数据存储
|
||||||
|
- Chrome Storage API: `utils/chromeStorage.ts` (用于配置等小数据)
|
||||||
|
- IndexedDB: `utils/recordEventsDb.ts` (用于存储大量录制事件)
|
||||||
|
|
||||||
|
### 关键配置文件
|
||||||
|
|
||||||
|
#### wxt.config.ts
|
||||||
|
- 配置 WXT 框架参数
|
||||||
|
- 启用 React 模块
|
||||||
|
- 配置浏览器扩展权限
|
||||||
|
- Vite 构建配置(使用 Terser 压缩,强制 ASCII 编码)
|
||||||
|
|
||||||
|
#### manifest 权限
|
||||||
|
```typescript
|
||||||
|
permissions: [
|
||||||
|
'storage', // 存储权限
|
||||||
|
'unlimitedStorage', // 无限制存储
|
||||||
|
'clipboardWrite', // 剪贴板写入
|
||||||
|
'activeTab', // 当前标签页
|
||||||
|
'scripting', // 脚本注入
|
||||||
|
'tabs', // 标签页管理
|
||||||
|
'offscreen', // 离屏文档
|
||||||
|
'downloads', // 下载管理
|
||||||
|
'debugger', // 调试器
|
||||||
|
],
|
||||||
|
host_permissions: ['<all_urls>'] // 访问所有网站
|
||||||
|
````
|
||||||
|
|
||||||
|
## 开发注意事项
|
||||||
|
|
||||||
|
### 扩展入口点
|
||||||
|
|
||||||
|
- **后台脚本**: `entrypoints/background.ts` - 处理扩展生命周期和后台任务
|
||||||
|
- **内容脚本**: `entrypoints/content.ts` - 注入到网页中,处理 DOM 交互
|
||||||
|
- **弹窗**: `entrypoints/popup/main.tsx` - 用户点击扩展图标时显示
|
||||||
|
- **离屏文档**: `entrypoints/offscreen/main.tsx` - 处理长时间运行的任务(如录制)
|
||||||
|
|
||||||
|
### 浏览器兼容性
|
||||||
|
|
||||||
|
- 支持 Chrome 和 Firefox 浏览器
|
||||||
|
- 使用 WXT 框架抽象浏览器差异
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- 使用 ESLint 进行代码检查
|
||||||
|
- Husky 用于 Git 钩子管理
|
||||||
|
- Lint-staged 确保暂存文件符合规范
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
@@ -12,32 +12,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { TIME_ZONE_LIST, TIMESTAMP_UNITS } from './constants';
|
||||||
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)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function DatetimeToTimestamp() {
|
export function DatetimeToTimestamp() {
|
||||||
const [dateValue, setDateValue] = useState(() => dayjs().format('YYYY/MM/DD HH:mm:ss'));
|
const [dateValue, setDateValue] = useState(() => dayjs().format('YYYY/MM/DD HH:mm:ss'));
|
||||||
@@ -52,7 +27,8 @@ export function DatetimeToTimestamp() {
|
|||||||
setError('请输入有效的日期时间');
|
setError('请输入有效的日期时间');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const timestamp = dayjs.tz(currentDate, zone);
|
// 使用 tz 方法直接解析带时区的日期
|
||||||
|
const timestamp = dayjs.tz(currentDate, 'YYYY/MM/DD HH:mm:ss', zone);
|
||||||
if (!timestamp.isValid()) {
|
if (!timestamp.isValid()) {
|
||||||
setError('无效的日期时间格式');
|
setError('无效的日期时间格式');
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const RoutePersistence = () => {
|
|||||||
|
|
||||||
if (lastRoute && lastRoute !== '/' && location.pathname === '/') {
|
if (lastRoute && lastRoute !== '/' && location.pathname === '/') {
|
||||||
navigate(lastRoute, { replace: true });
|
navigate(lastRoute, { replace: true });
|
||||||
console.log('跳转路由', lastRoute);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('恢复路由失败', err);
|
console.error('恢复路由失败', err);
|
||||||
@@ -26,17 +25,16 @@ const RoutePersistence = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
restoreRoute().then(() => console.info('恢复路由成功'));
|
restoreRoute();
|
||||||
}, [location, navigate]);
|
}, [navigate, location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saveRoute = async () => {
|
const saveRoute = async () => {
|
||||||
if (!isRestored.current) return;
|
if (!isRestored.current) return;
|
||||||
await storageUtil.set('app/lastRoute', location.pathname);
|
await storageUtil.set('app/lastRoute', location.pathname);
|
||||||
console.log('保存路由', location.pathname);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
saveRoute().then(() => console.info('保存路由成功'));
|
saveRoute();
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -12,37 +12,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import { TIME_ZONE_LIST, TIMESTAMP_UNITS } from './constants';
|
||||||
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)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function TimestampToDatetime() {
|
export function TimestampToDatetime() {
|
||||||
const [timestampValue, setTimestampValue] = useState(() => dayjs().valueOf().toString());
|
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'];
|
||||||
Generated
+1733
-64
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -14,7 +14,10 @@
|
|||||||
"compile": "tsc --noEmit",
|
"compile": "tsc --noEmit",
|
||||||
"postinstall": "wxt prepare",
|
"postinstall": "wxt prepare",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"lint": "eslint . --max-warnings=0"
|
"lint": "eslint . --max-warnings=0",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@@ -49,17 +52,20 @@
|
|||||||
"@types/webextension-polyfill": "^0.12.4",
|
"@types/webextension-polyfill": "^0.12.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@wxt-dev/module-react": "^1.1.5",
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"globals": "^17.2.0",
|
"globals": "^17.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jsdom": "^29.0.0",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.2.7",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"terser": "^5.46.0",
|
"terser": "^5.46.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
|
"vitest": "^4.1.0",
|
||||||
"wxt": "^0.20.6"
|
"wxt": "^0.20.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -52,5 +52,13 @@
|
|||||||
"components",
|
"components",
|
||||||
"eslint.config.ts"
|
"eslint.config.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", ".wxt"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".wxt",
|
||||||
|
"components/__tests__",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.setup.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vitest/globals", "node"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"components/**/*.test.tsx",
|
||||||
|
"components/**/*.tsx",
|
||||||
|
"components/**/*.ts",
|
||||||
|
"utils/**/*.ts",
|
||||||
|
"utils/**/*.tsx",
|
||||||
|
"vitest.setup.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './vitest.setup.ts',
|
||||||
|
cache: {
|
||||||
|
dir: resolve(__dirname, '.vitest'),
|
||||||
|
},
|
||||||
|
tsconfig: './tsconfig.vitest.json',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// 模拟 chrome storage API
|
||||||
|
Object.defineProperty(global, 'chrome', {
|
||||||
|
value: {
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: vi.fn(),
|
||||||
|
set: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟 navigator.clipboard
|
||||||
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
|
value: {
|
||||||
|
writeText: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user