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-*.json
|
||||
.wxt
|
||||
.vitest
|
||||
|
||||
# Editor directories and files
|
||||
.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,
|
||||
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'];
|
||||
Generated
+1733
-64
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -14,7 +14,10 @@
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare",
|
||||
"prepare": "husky",
|
||||
"lint": "eslint . --max-warnings=0"
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@@ -49,17 +52,20 @@
|
||||
"@types/webextension-polyfill": "^0.12.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^17.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.8.1",
|
||||
"terser": "^5.46.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vitest": "^4.1.0",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -52,5 +52,13 @@
|
||||
"components",
|
||||
"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