diff --git a/docs/TEST_GUIDE.md b/docs/TEST_GUIDE.md
deleted file mode 100644
index c7d9598d..00000000
--- a/docs/TEST_GUIDE.md
+++ /dev/null
@@ -1,338 +0,0 @@
-# 崩溃修复测试指南
-
-> 测试时间:2025-10-14
-> 测试范围:SignInIllustration.js + SignUpIllustration.js
-> 服务器地址:http://localhost:3000
-
----
-
-## 🎯 测试目标
-
-验证以下修复是否有效:
-- ✅ 响应对象崩溃(6处)
-- ✅ 组件卸载后 setState(6处)
-- ✅ 定时器内存泄漏(2处)
-
----
-
-## 📋 测试清单
-
-### ✅ 关键测试(必做)
-
-#### 1. **网络异常测试** - 验证响应对象修复
-
-**登录页面 - 发送验证码**
-```
-测试步骤:
-1. 打开 http://localhost:3000/auth/sign-in
-2. 切换到"验证码登录"模式
-3. 输入手机号:13800138000
-4. 打开浏览器开发者工具 (F12) → Network 标签
-5. 点击 Offline 模拟断网
-6. 点击"发送验证码"按钮
-
-预期结果:
-✅ 显示错误提示:"发送验证码失败 - 网络请求失败,请检查网络连接"
-✅ 页面不崩溃
-✅ 无 JavaScript 错误
-
-修复前:
-❌ 页面白屏崩溃
-❌ Console 报错:Cannot read property 'json' of null
-```
-
-**登录页面 - 微信登录**
-```
-测试步骤:
-1. 在登录页面,保持断网状态
-2. 点击"扫码登录"按钮
-
-预期结果:
-✅ 显示错误提示:"获取微信授权失败 - 网络请求失败,请检查网络连接"
-✅ 页面不崩溃
-✅ 无 JavaScript 错误
-```
-
-**注册页面 - 发送验证码**
-```
-测试步骤:
-1. 打开 http://localhost:3000/auth/sign-up
-2. 切换到"验证码注册"模式
-3. 输入手机号:13800138000
-4. 保持断网状态
-5. 点击"发送验证码"按钮
-
-预期结果:
-✅ 显示错误提示:"发送失败 - 网络请求失败..."
-✅ 页面不崩溃
-```
-
----
-
-#### 2. **组件卸载测试** - 验证内存泄漏修复
-
-**倒计时中离开页面**
-```
-测试步骤:
-1. 恢复网络连接
-2. 在登录页面输入手机号并发送验证码
-3. 等待倒计时开始(60秒倒计时)
-4. 立即点击浏览器后退按钮或切换到其他页面
-5. 打开 Console 查看是否有警告
-
-预期结果:
-✅ 无警告:"Can't perform a React state update on an unmounted component"
-✅ 倒计时定时器正确清理
-✅ 无内存泄漏
-
-修复前:
-❌ Console 警告:Memory leak warning
-❌ setState 在组件卸载后仍被调用
-```
-
-**请求进行中离开页面**
-```
-测试步骤:
-1. 在注册页面填写完整信息
-2. 点击"注册"按钮
-3. 在请求响应前(loading 状态)快速刷新页面或关闭标签页
-4. 打开新标签页查看 Console
-
-预期结果:
-✅ 无崩溃
-✅ 无警告信息
-✅ 请求被正确取消或忽略
-```
-
-**注册成功跳转前离开**
-```
-测试步骤:
-1. 完成注册提交
-2. 在显示"注册成功"提示后
-3. 立即关闭标签页(不等待2秒自动跳转)
-
-预期结果:
-✅ 无警告
-✅ navigate 不会在组件卸载后执行
-```
-
----
-
-#### 3. **边界情况测试** - 验证数据完整性检查
-
-**后端返回空响应**
-```
-测试步骤(需要模拟后端):
-1. 使用 Chrome DevTools → Network → 右键请求 → Edit and Resend
-2. 修改响应为空对象 {}
-3. 观察页面反应
-
-预期结果:
-✅ 显示错误:"服务器响应为空"
-✅ 不会尝试访问 undefined 属性
-✅ 页面不崩溃
-```
-
-**后端返回 500 错误**
-```
-测试步骤:
-1. 在登录页面点击"扫码登录"
-2. 如果后端返回 500 错误
-
-预期结果:
-✅ 显示错误:"获取二维码失败:HTTP 500"
-✅ 页面不崩溃
-```
-
----
-
-### 🧪 进阶测试(推荐)
-
-#### 4. **弱网环境测试**
-
-**慢速网络模拟**
-```
-测试步骤:
-1. Chrome DevTools → Network → Throttling → Slow 3G
-2. 尝试发送验证码
-3. 等待 10 秒(超时时间)
-
-预期结果:
-✅ 10秒后显示超时错误
-✅ 不会无限等待
-✅ 用户可以重试
-```
-
-**丢包模拟**
-```
-测试步骤:
-1. 使用 Chrome DevTools 模拟丢包
-2. 连续点击"发送验证码"多次
-
-预期结果:
-✅ 每次请求都有适当的错误提示
-✅ 不会因为并发请求而崩溃
-✅ 按钮在请求期间正确禁用
-```
-
----
-
-#### 5. **定时器清理测试**
-
-**倒计时清理验证**
-```
-测试步骤:
-1. 在登录页面发送验证码
-2. 等待倒计时到 50 秒
-3. 快速切换到注册页面
-4. 再切换回登录页面
-5. 观察倒计时是否重置
-
-预期结果:
-✅ 定时器在页面切换时正确清理
-✅ 返回登录页面时倒计时重新开始(如果再次发送)
-✅ 没有多个定时器同时运行
-```
-
----
-
-#### 6. **并发请求测试**
-
-**快速连续点击**
-```
-测试步骤:
-1. 在登录页面输入手机号
-2. 快速连续点击"发送验证码"按钮 5 次
-
-预期结果:
-✅ 只发送一次请求(按钮在请求期间禁用)
-✅ 不会因为并发而崩溃
-✅ 正确显示 loading 状态
-```
-
----
-
-## 🔍 监控指标
-
-### Console 检查清单
-
-在测试过程中,打开 Console (F12) 监控以下内容:
-
-```
-✅ 无红色错误(Error)
-✅ 无内存泄漏警告(Memory leak warning)
-✅ 无 setState 警告(Can't perform a React state update...)
-✅ 无 undefined 访问错误(Cannot read property of undefined)
-```
-
-### Network 检查清单
-
-打开 Network 标签监控:
-
-```
-✅ 请求超时时间:10秒
-✅ 失败请求有正确的错误处理
-✅ 没有重复的请求
-✅ 请求被正确取消(如果页面卸载)
-```
-
-### Performance 检查清单
-
-打开 Performance 标签(可选):
-
-```
-✅ 无内存泄漏(Memory 不会持续增长)
-✅ 定时器正确清理(Timer count 正确)
-✅ EventListener 正确清理
-```
-
----
-
-## 📊 测试记录表
-
-请在测试时填写以下表格:
-
-| 测试项 | 状态 | 问题描述 | 截图 |
-|--------|------|---------|------|
-| 登录页 - 断网发送验证码 | ⬜ 通过 / ⬜ 失败 | | |
-| 登录页 - 断网微信登录 | ⬜ 通过 / ⬜ 失败 | | |
-| 注册页 - 断网发送验证码 | ⬜ 通过 / ⬜ 失败 | | |
-| 倒计时中离开页面 | ⬜ 通过 / ⬜ 失败 | | |
-| 请求进行中离开页面 | ⬜ 通过 / ⬜ 失败 | | |
-| 注册成功跳转前离开 | ⬜ 通过 / ⬜ 失败 | | |
-| 后端返回空响应 | ⬜ 通过 / ⬜ 失败 | | |
-| 慢速网络超时 | ⬜ 通过 / ⬜ 失败 | | |
-| 定时器清理 | ⬜ 通过 / ⬜ 失败 | | |
-| 并发请求 | ⬜ 通过 / ⬜ 失败 | | |
-
----
-
-## 🐛 如何报告问题
-
-如果发现问题,请提供:
-
-1. **测试场景**:具体的测试步骤
-2. **预期结果**:应该发生什么
-3. **实际结果**:实际发生了什么
-4. **Console 错误**:完整的错误信息
-5. **截图/录屏**:问题的视觉证明
-6. **环境信息**:
- - 浏览器版本
- - 操作系统
- - 网络状态
-
----
-
-## ✅ 测试完成检查
-
-测试完成后,确认以下内容:
-
-```
-□ 所有关键测试通过
-□ Console 无错误
-□ Network 请求正常
-□ 无内存泄漏警告
-□ 用户体验流畅
-```
-
----
-
-## 🎯 快速测试命令
-
-```bash
-# 1. 确认服务器运行
-curl http://localhost:3000
-
-# 2. 打开浏览器测试
-open http://localhost:3000/auth/sign-in
-
-# 3. 查看编译日志
-tail -f /tmp/react-build.log
-```
-
----
-
-## 📱 测试页面链接
-
-- **登录页面**: http://localhost:3000/auth/sign-in
-- **注册页面**: http://localhost:3000/auth/sign-up
-- **首页**: http://localhost:3000/home
-
----
-
-## 🔧 开发者工具快捷键
-
-```
-F12 - 打开开发者工具
-Ctrl/Cmd+R - 刷新页面
-Ctrl/Cmd+Shift+R - 强制刷新(清除缓存)
-Ctrl/Cmd+Shift+C - 元素选择器
-```
-
----
-
-**测试时间**:2025-10-14
-**预计测试时长**:15-30 分钟
-**建议测试人员**:开发者 + QA
-
-祝测试顺利!如发现问题请及时反馈。
diff --git a/docs/test-cases/notification-tests.md b/docs/test-cases/notification-tests.md
deleted file mode 100644
index 47cb8a93..00000000
--- a/docs/test-cases/notification-tests.md
+++ /dev/null
@@ -1,1770 +0,0 @@
-# 消息通知系统 - 自动化测试用例
-
-> **文档版本**: v2.0.0
-> **更新日期**: 2025-01-07
-> **测试框架**: Jest + React Testing Library
->
-> 📖 **相关文档**: [手动测试指南](./notification-manual-testing-guide.md)
-
----
-
-## 📑 目录
-
-1. [如何开发测试用例](#-如何开发测试用例)
-2. [如何运行测试用例](#-如何运行测试用例)
-3. [手动测试 vs 自动化测试](#-手动测试-vs-自动化测试)
-4. [测试环境配置](#-测试环境配置)
-5. [单元测试](#-单元测试)
-6. [集成测试](#-集成测试)
-7. [E2E测试](#-e2e测试)
-8. [性能测试](#-性能测试)
-9. [测试覆盖率报告](#-测试覆盖率报告)
-
----
-
-## 💡 如何开发测试用例
-
-### 测试驱动开发(TDD)流程
-
-遵循 **Red-Green-Refactor** 循环:
-
-```
-1. Red(红) → 先写测试,运行失败(红色)
-2. Green(绿) → 编写最少代码使测试通过(绿色)
-3. Refactor(重构) → 优化代码,保持测试通过
-```
-
-### 测试金字塔
-
-```
- /\
- / \ E2E Tests (10%)
- /____\ - 慢,昂贵,脆弱
- / \ - 测试关键用户流程
- /________\
- / \ Integration Tests (20%)
- /____________\ - 测试组件间协作
-/______________\ Unit Tests (70%)
- - 快,稳定,便宜
- - 测试单个函数/组件
-```
-
-### 文件命名规范
-
-```bash
-src/
-├── components/
-│ ├── NotificationContainer/
-│ │ ├── index.js
-│ │ └── __tests__/
-│ │ └── NotificationContainer.test.js # ✅ 组件测试
-├── services/
-│ ├── notificationHistoryService.js
-│ └── __tests__/
-│ └── notificationHistoryService.test.js # ✅ 服务测试
-├── constants/
-│ ├── notificationTypes.js
-│ └── __tests__/
-│ └── notificationTypes.test.js # ✅ 常量测试
-└── contexts/
- ├── NotificationContext.js
- └── __tests__/
- └── NotificationContext.test.js # ✅ Context 测试
-```
-
-### 测试结构(AAA 模式)
-
-**Arrange - Act - Assert**
-
-```javascript
-test('应该正确添加通知到历史记录', () => {
- // Arrange(准备)- 设置测试数据和环境
- const notification = {
- id: 'test001',
- type: 'announcement',
- title: '测试通知',
- content: '测试内容',
- publishTime: Date.now(),
- };
-
- // Act(执行)- 调用要测试的函数
- notificationHistoryService.add(notification);
-
- // Assert(断言)- 验证结果
- const history = notificationHistoryService.getAll();
- expect(history).toHaveLength(1);
- expect(history[0].title).toBe('测试通知');
-});
-```
-
-### 测试编写最佳实践
-
-#### 1. 每个测试只测一个功能
-
-❌ **不推荐**(测试多个功能):
-```javascript
-test('通知系统功能测试', () => {
- // 测试添加
- service.add(notification);
- expect(service.getAll()).toHaveLength(1);
-
- // 测试筛选
- expect(service.filter({ type: 'announcement' })).toHaveLength(1);
-
- // 测试删除
- service.clear();
- expect(service.getAll()).toHaveLength(0);
-});
-```
-
-✅ **推荐**(拆分成独立测试):
-```javascript
-test('应该正确添加通知', () => {
- service.add(notification);
- expect(service.getAll()).toHaveLength(1);
-});
-
-test('应该正确筛选通知', () => {
- service.add(notification);
- expect(service.filter({ type: 'announcement' })).toHaveLength(1);
-});
-
-test('应该正确清空通知', () => {
- service.add(notification);
- service.clear();
- expect(service.getAll()).toHaveLength(0);
-});
-```
-
-#### 2. 使用描述性的测试名称
-
-❌ **不推荐**:
-```javascript
-test('test 1', () => { ... });
-test('works', () => { ... });
-```
-
-✅ **推荐**:
-```javascript
-test('应该在收到新事件时添加到通知列表', () => { ... });
-test('应该在紧急通知时不自动关闭', () => { ... });
-test('应该在权限被拒绝时不触发浏览器通知', () => { ... });
-```
-
-#### 3. 使用 Mock 隔离外部依赖
-
-```javascript
-// Mock localStorage
-beforeEach(() => {
- localStorage.clear();
- localStorage.setItem.mockClear();
-});
-
-// Mock Audio API
-global.Audio = jest.fn().mockImplementation(() => ({
- play: jest.fn().mockResolvedValue(undefined),
-}));
-
-// Mock Notification API
-global.Notification = {
- permission: 'granted',
- requestPermission: jest.fn().mockResolvedValue('granted'),
-};
-```
-
-#### 4. 清理测试环境
-
-```javascript
-describe('notificationHistoryService', () => {
- beforeEach(() => {
- // 每个测试前清理
- localStorage.clear();
- });
-
- afterEach(() => {
- // 每个测试后清理(如果需要)
- jest.clearAllMocks();
- });
-
- test('测试用例', () => { ... });
-});
-```
-
-### 常见测试模式
-
-#### 模式1: 测试纯函数
-
-```javascript
-// src/utils/formatTime.js
-export const formatNotificationTime = (timestamp) => {
- const diff = Date.now() - timestamp;
- if (diff < 60000) return '刚刚';
- if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
- return '很久前';
-};
-
-// __tests__/formatTime.test.js
-describe('formatNotificationTime', () => {
- test('应该返回"刚刚"当时间小于1分钟', () => {
- const timestamp = Date.now() - 30000; // 30秒前
- expect(formatNotificationTime(timestamp)).toBe('刚刚');
- });
-
- test('应该返回正确的分钟数', () => {
- const timestamp = Date.now() - 120000; // 2分钟前
- expect(formatNotificationTime(timestamp)).toBe('2分钟前');
- });
-});
-```
-
-#### 模式2: 测试 React 组件
-
-```javascript
-// __tests__/NotificationCard.test.js
-import { render, screen, fireEvent } from '@testing-library/react';
-import NotificationCard from '../NotificationCard';
-
-describe('NotificationCard', () => {
- const mockNotification = {
- id: 'test001',
- type: 'announcement',
- priority: 'important',
- title: '测试通知',
- content: '测试内容',
- };
-
- test('应该渲染通知标题和内容', () => {
- render();
-
- expect(screen.getByText('测试通知')).toBeInTheDocument();
- expect(screen.getByText('测试内容')).toBeInTheDocument();
- });
-
- test('应该在点击关闭按钮时调用 onClose', () => {
- const mockOnClose = jest.fn();
- render();
-
- const closeButton = screen.getByRole('button', { name: /关闭/i });
- fireEvent.click(closeButton);
-
- expect(mockOnClose).toHaveBeenCalledWith('test001');
- });
-});
-```
-
-#### 模式3: 测试异步逻辑
-
-```javascript
-// __tests__/browserNotificationService.test.js
-describe('browserNotificationService', () => {
- test('应该在权限授予时显示浏览器通知', async () => {
- global.Notification.permission = 'granted';
- const mockNotification = { title: '测试', body: '内容' };
-
- await browserNotificationService.show(mockNotification);
-
- // 验证 Notification 构造函数被调用
- expect(global.Notification).toHaveBeenCalledWith('测试', expect.any(Object));
- });
-});
-```
-
----
-
-## 🚀 如何运行测试用例
-
-### 基础命令
-
-```bash
-# 运行所有测试
-npm test
-
-# 运行所有测试(CI模式,运行一次后退出)
-npm test -- --watchAll=false
-
-# 监视模式(修改文件时自动重新运行)
-npm test -- --watch
-
-# 运行特定文件的测试
-npm test notificationTypes.test.js
-
-# 运行匹配模式的测试
-npm test -- --testNamePattern="应该正确添加通知"
-
-# 生成覆盖率报告
-npm test -- --coverage
-
-# 生成覆盖率并在浏览器中查看
-npm test -- --coverage && open coverage/lcov-report/index.html
-```
-
-### 运行特定测试套件
-
-```bash
-# 只运行单元测试
-npm test -- --testPathPattern=__tests__
-
-# 只运行集成测试
-npm test -- --testPathPattern=integration
-
-# 只运行 E2E 测试
-npm test -- --testPathPattern=e2e
-```
-
-### 调试测试
-
-#### 方法1: 使用 console.log
-
-```javascript
-test('调试示例', () => {
- const result = someFunction();
- console.log('结果:', result); // 在控制台输出
- expect(result).toBe(expected);
-});
-```
-
-#### 方法2: 使用 VS Code 调试
-
-在 `.vscode/launch.json` 中添加:
-
-```json
-{
- "version": "0.2.0",
- "configurations": [
- {
- "type": "node",
- "request": "launch",
- "name": "Jest 当前文件",
- "program": "${workspaceFolder}/node_modules/.bin/jest",
- "args": [
- "${fileBasename}",
- "--runInBand",
- "--no-cache",
- "--watchAll=false"
- ],
- "console": "integratedTerminal",
- "internalConsoleOptions": "neverOpen"
- }
- ]
-}
-```
-
-#### 方法3: 使用 test.only
-
-```javascript
-test.only('只运行这个测试', () => {
- // 其他测试会被跳过
- expect(1 + 1).toBe(2);
-});
-
-test('这个测试不会运行', () => {
- expect(2 + 2).toBe(4);
-});
-```
-
-### 持续集成(CI/CD)
-
-在 GitHub Actions、GitLab CI 等平台中运行测试:
-
-```yaml
-# .github/workflows/test.yml
-name: Tests
-
-on: [push, pull_request]
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Setup Node.js
- uses: actions/setup-node@v2
- with:
- node-version: '14'
-
- - name: Install dependencies
- run: npm install
-
- - name: Run tests
- run: npm test -- --watchAll=false --coverage
-
- - name: Upload coverage
- uses: codecov/codecov-action@v2
-```
-
----
-
-## ⚖️ 手动测试 vs 自动化测试
-
-### 自动化测试适用场景
-
-✅ **应该用自动化测试**:
-
-| 场景 | 原因 | 示例 |
-|------|------|------|
-| 单元逻辑测试 | 快速、稳定、可重复 | 工具函数、数据处理逻辑 |
-| 组件渲染测试 | 验证输出是否正确 | 通知卡片是否显示正确内容 |
-| API 响应测试 | Mock 数据验证业务逻辑 | Socket 消息处理逻辑 |
-| 回归测试 | 防止修改破坏现有功能 | 每次提交前运行全部测试 |
-| 边界条件测试 | 覆盖各种输入情况 | 空数据、超长文本、特殊字符 |
-
-**自动化测试覆盖率目标**:
-- 业务逻辑层: **80%+**
-- 关键服务: **90%+**
-- 工具函数: **95%+**
-
-### 手动测试必需场景
-
-✅ **必须用手动测试**:
-
-| 场景 | 原因 | 示例 |
-|------|------|------|
-| UI/UX 体验 | 自动化测试无法评估"用户感受" | 动画流畅度、视觉美观度、交互自然度 |
-| 浏览器兼容性 | 需要在真实浏览器中验证 | Chrome、Edge、Firefox、Safari 表现 |
-| 浏览器原生 API | Mock 无法完全模拟真实行为 | Notification API、Audio API、WebSocket |
-| 网络环境 | 需要真实网络条件 | 断线重连、高延迟、弱网测试 |
-| 用户流程 | 验证完整的用户旅程 | 从打开网站 → 授权 → 接收通知 → 点击跳转 |
-| 性能测试 | 需要真实环境数据 | 大量通知时的内存占用、渲染性能 |
-| 探索性测试 | 发现意料之外的问题 | 用户可能进行的非预期操作 |
-
-### 对比总结
-
-| | 自动化测试 | 手动测试 |
-|--|-----------|---------|
-| **速度** | 🚀 快(秒级) | 🐢 慢(分钟级) |
-| **成本** | 💰 初期高(编写成本),长期低 | 💰💰 持续高(人力成本) |
-| **稳定性** | ✅ 高(可重复) | ⚠️ 低(人为误差) |
-| **覆盖面** | 📊 窄(只测定义的场景) | 🌐 广(可探索未知问题) |
-| **运行时机** | 🔄 每次提交、CI/CD | 👨💻 发布前、重大更新 |
-| **适用对象** | 🔧 开发人员 | 👥 测试人员 + 开发人员 |
-
-### 最佳实践:两者结合
-
-```
-开发阶段:
-1. TDD 开发 → 先写自动化测试
-2. 实现功能 → 使测试通过
-3. 手动验证 → 检查 UI 和交互
-
-提交前:
-1. 运行所有自动化测试 → npm test
-2. 快速手动测试 → 关键流程验证
-
-发布前:
-1. 完整回归测试 → 自动化测试套件
-2. 完整手动测试 → 按手动测试清单逐项验证
-3. 跨浏览器测试 → Chrome、Edge、Firefox、Safari
-```
-
-### 结论
-
-> **开发了自动化测试用例后,仍然需要手动测试!**
->
-> - **自动化测试**:保证**功能正确性**和**代码质量**(70-80%覆盖率)
-> - **手动测试**:关注**用户体验**和**边缘场景**(20-30%)
-> - **两者互补**:确保系统既稳定又好用
-
-**推荐测试策略**:
-```
-每次提交: 自动化测试 (100%) + 手动测试 (关键流程)
-每周发布: 自动化测试 (100%) + 手动测试 (完整清单)
-重大版本: 自动化测试 (100%) + 手动测试 (完整清单) + 外部测试人员
-```
-
----
-
-## 🛠️ 测试环境配置
-
-### 安装依赖
-
-```bash
-npm install --save-dev \
- @testing-library/react \
- @testing-library/jest-dom \
- @testing-library/user-event \
- @testing-library/react-hooks \
- jest-localstorage-mock
-```
-
-### Jest 配置
-
-`jest.config.js`:
-```javascript
-module.exports = {
- testEnvironment: 'jsdom',
- setupFilesAfterEnv: ['/src/setupTests.js'],
- moduleNameMapper: {
- '^@/(.*)$': '/src/$1',
- '^@constants/(.*)$': '/src/constants/$1',
- '^@services/(.*)$': '/src/services/$1',
- '^@contexts/(.*)$': '/src/contexts/$1',
- '^@components/(.*)$': '/src/components/$1',
- '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
- },
- collectCoverageFrom: [
- 'src/**/*.{js,jsx}',
- '!src/index.js',
- '!src/reportWebVitals.js',
- '!src/**/*.test.{js,jsx}',
- ],
-};
-```
-
-### 测试配置文件
-
-`src/setupTests.js`:
-```javascript
-import '@testing-library/jest-dom';
-import 'jest-localstorage-mock';
-
-// Mock Audio API
-global.Audio = jest.fn().mockImplementation(() => ({
- play: jest.fn().mockResolvedValue(undefined),
- pause: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
-}));
-
-// Mock Notification API
-global.Notification = {
- permission: 'default',
- requestPermission: jest.fn().mockResolvedValue('granted'),
-};
-
-// Mock window.matchMedia
-Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: jest.fn().mockImplementation(query => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
- })),
-});
-```
-
----
-
-## 🧪 单元测试
-
-### 1. notificationTypes.js - 类型定义测试
-
-`src/constants/__tests__/notificationTypes.test.js`:
-```javascript
-import {
- NOTIFICATION_TYPES,
- PRIORITY_LEVELS,
- NOTIFICATION_TYPE_CONFIGS,
- PRIORITY_CONFIGS,
- getNotificationTypeConfig,
- getPriorityConfig,
-} from '../notificationTypes';
-
-describe('notificationTypes', () => {
- describe('常量定义', () => {
- test('应该定义所有通知类型', () => {
- expect(NOTIFICATION_TYPES.ANNOUNCEMENT).toBe('announcement');
- expect(NOTIFICATION_TYPES.STOCK_ALERT).toBe('stock_alert');
- expect(NOTIFICATION_TYPES.EVENT_ALERT).toBe('event_alert');
- expect(NOTIFICATION_TYPES.ANALYSIS_REPORT).toBe('analysis_report');
- });
-
- test('应该定义所有优先级', () => {
- expect(PRIORITY_LEVELS.URGENT).toBe('urgent');
- expect(PRIORITY_LEVELS.IMPORTANT).toBe('important');
- expect(PRIORITY_LEVELS.NORMAL).toBe('normal');
- });
- });
-
- describe('类型配置', () => {
- test('每个通知类型应该有完整的配置', () => {
- Object.values(NOTIFICATION_TYPES).forEach(type => {
- const config = NOTIFICATION_TYPE_CONFIGS[type];
- expect(config).toBeDefined();
- expect(config).toHaveProperty('name');
- expect(config).toHaveProperty('icon');
- expect(config).toHaveProperty('colorScheme');
- expect(config).toHaveProperty('bg');
- expect(config).toHaveProperty('borderColor');
- expect(config).toHaveProperty('iconColor');
- expect(config).toHaveProperty('hoverBg');
- });
- });
-
- test('每个优先级应该有完整的配置', () => {
- Object.values(PRIORITY_LEVELS).forEach(priority => {
- const config = PRIORITY_CONFIGS[priority];
- expect(config).toBeDefined();
- expect(config).toHaveProperty('label');
- expect(config).toHaveProperty('colorScheme');
- expect(config).toHaveProperty('show');
- expect(config).toHaveProperty('borderWidth');
- expect(config).toHaveProperty('bgOpacity');
- });
- });
- });
-
- describe('辅助函数', () => {
- test('getNotificationTypeConfig 应该返回正确的配置', () => {
- const config = getNotificationTypeConfig(NOTIFICATION_TYPES.ANNOUNCEMENT);
- expect(config.name).toBe('公告通知');
- expect(config.colorScheme).toBe('blue');
- });
-
- test('getPriorityConfig 应该返回正确的配置', () => {
- const config = getPriorityConfig(PRIORITY_LEVELS.URGENT);
- expect(config.label).toBe('紧急');
- expect(config.colorScheme).toBe('red');
- });
-
- test('传入无效类型应该返回默认配置', () => {
- const config = getNotificationTypeConfig('invalid_type');
- expect(config).toBeDefined();
- });
- });
-});
-```
-
----
-
-### 2. notificationHistoryService.js - 历史记录服务测试
-
-`src/services/__tests__/notificationHistoryService.test.js`:
-```javascript
-import notificationHistoryService from '../notificationHistoryService';
-import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '@constants/notificationTypes';
-
-describe('notificationHistoryService', () => {
- beforeEach(() => {
- localStorage.clear();
- jest.clearAllMocks();
- });
-
- describe('saveNotification', () => {
- test('应该保存通知到localStorage', () => {
- const notification = {
- id: 'test001',
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '测试通知',
- content: '测试内容',
- };
-
- notificationHistoryService.saveNotification(notification);
-
- const { records } = notificationHistoryService.getHistory();
- expect(records).toHaveLength(1);
- expect(records[0].notification.id).toBe('test001');
- expect(records[0].notification.title).toBe('测试通知');
- });
-
- test('应该自动添加时间戳', () => {
- const notification = { id: 'test001', title: '测试' };
- notificationHistoryService.saveNotification(notification);
-
- const { records } = notificationHistoryService.getHistory();
- expect(records[0]).toHaveProperty('receivedAt');
- expect(records[0]).toHaveProperty('readAt');
- expect(records[0]).toHaveProperty('clickedAt');
- });
-
- test('应该限制最大存储数量', () => {
- // 保存600条通知(超过最大500条)
- for (let i = 0; i < 600; i++) {
- notificationHistoryService.saveNotification({
- id: `test${i}`,
- title: `测试${i}`,
- });
- }
-
- const { total } = notificationHistoryService.getHistory();
- expect(total).toBe(500);
- });
- });
-
- describe('getHistory', () => {
- beforeEach(() => {
- // 准备测试数据
- notificationHistoryService.saveNotification({
- id: 'test001',
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.URGENT,
- title: '公告1',
- });
- notificationHistoryService.saveNotification({
- id: 'test002',
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '事件1',
- });
- notificationHistoryService.saveNotification({
- id: 'test003',
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '公告2',
- });
- });
-
- test('应该返回所有历史记录', () => {
- const { records, total } = notificationHistoryService.getHistory();
- expect(total).toBe(3);
- expect(records).toHaveLength(3);
- });
-
- test('应该按类型筛选', () => {
- const { records } = notificationHistoryService.getHistory({
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- });
- expect(records).toHaveLength(2);
- expect(records.every(r => r.notification.type === NOTIFICATION_TYPES.ANNOUNCEMENT)).toBe(true);
- });
-
- test('应该按优先级筛选', () => {
- const { records } = notificationHistoryService.getHistory({
- priority: PRIORITY_LEVELS.URGENT,
- });
- expect(records).toHaveLength(1);
- expect(records[0].notification.priority).toBe(PRIORITY_LEVELS.URGENT);
- });
-
- test('应该按日期范围筛选', () => {
- const now = Date.now();
- const { records } = notificationHistoryService.getHistory({
- startDate: now - 1000,
- endDate: now + 1000,
- });
- expect(records).toHaveLength(3);
- });
-
- test('应该支持分页', () => {
- const { records, page, totalPages } = notificationHistoryService.getHistory({
- page: 1,
- pageSize: 2,
- });
- expect(records).toHaveLength(2);
- expect(page).toBe(1);
- expect(totalPages).toBe(2);
- });
- });
-
- describe('searchHistory', () => {
- beforeEach(() => {
- notificationHistoryService.saveNotification({
- id: 'test001',
- title: '央行宣布降准',
- content: '中国人民银行宣布...',
- });
- notificationHistoryService.saveNotification({
- id: 'test002',
- title: '贵州茅台发布财报',
- content: '2024年度营收...',
- });
- });
-
- test('应该按关键词搜索', () => {
- const results = notificationHistoryService.searchHistory('央行');
- expect(results).toHaveLength(1);
- expect(results[0].notification.title).toContain('央行');
- });
-
- test('应该搜索标题和内容', () => {
- const results = notificationHistoryService.searchHistory('宣布');
- expect(results).toHaveLength(1);
- });
-
- test('搜索不存在的关键词应该返回空数组', () => {
- const results = notificationHistoryService.searchHistory('不存在');
- expect(results).toHaveLength(0);
- });
- });
-
- describe('markAsRead', () => {
- test('应该标记为已读', () => {
- const notification = { id: 'test001', title: '测试' };
- notificationHistoryService.saveNotification(notification);
-
- notificationHistoryService.markAsRead('test001');
-
- const { records } = notificationHistoryService.getHistory();
- expect(records[0].readAt).not.toBeNull();
- });
- });
-
- describe('markAsClicked', () => {
- test('应该标记为已点击', () => {
- const notification = { id: 'test001', title: '测试' };
- notificationHistoryService.saveNotification(notification);
-
- notificationHistoryService.markAsClicked('test001');
-
- const { records } = notificationHistoryService.getHistory();
- expect(records[0].clickedAt).not.toBeNull();
- });
- });
-
- describe('getStats', () => {
- beforeEach(() => {
- notificationHistoryService.saveNotification({
- id: 'test001',
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.URGENT,
- });
- notificationHistoryService.saveNotification({
- id: 'test002',
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- });
- notificationHistoryService.markAsRead('test001');
- notificationHistoryService.markAsClicked('test001');
- });
-
- test('应该返回统计数据', () => {
- const stats = notificationHistoryService.getStats();
- expect(stats.total).toBe(2);
- expect(stats.read).toBe(1);
- expect(stats.unread).toBe(1);
- expect(stats.clicked).toBe(1);
- expect(stats.clickRate).toBe(50);
- });
-
- test('应该按类型统计', () => {
- const stats = notificationHistoryService.getStats();
- expect(stats.byType[NOTIFICATION_TYPES.ANNOUNCEMENT]).toBe(1);
- expect(stats.byType[NOTIFICATION_TYPES.EVENT_ALERT]).toBe(1);
- });
-
- test('应该按优先级统计', () => {
- const stats = notificationHistoryService.getStats();
- expect(stats.byPriority[PRIORITY_LEVELS.URGENT]).toBe(1);
- expect(stats.byPriority[PRIORITY_LEVELS.IMPORTANT]).toBe(1);
- });
- });
-
- describe('导出功能', () => {
- beforeEach(() => {
- notificationHistoryService.saveNotification({
- id: 'test001',
- title: '测试1',
- });
- notificationHistoryService.saveNotification({
- id: 'test002',
- title: '测试2',
- });
- });
-
- test('downloadJSON应该创建下载链接', () => {
- const mockClick = jest.fn();
- const mockLink = {
- click: mockClick,
- setAttribute: jest.fn(),
- };
- document.createElement = jest.fn().mockReturnValue(mockLink);
-
- notificationHistoryService.downloadJSON();
-
- expect(mockLink.setAttribute).toHaveBeenCalledWith('download', expect.stringContaining('.json'));
- expect(mockClick).toHaveBeenCalled();
- });
-
- test('downloadCSV应该创建下载链接', () => {
- const mockClick = jest.fn();
- const mockLink = {
- click: mockClick,
- setAttribute: jest.fn(),
- };
- document.createElement = jest.fn().mockReturnValue(mockLink);
-
- notificationHistoryService.downloadCSV();
-
- expect(mockLink.setAttribute).toHaveBeenCalledWith('download', expect.stringContaining('.csv'));
- expect(mockClick).toHaveBeenCalled();
- });
- });
-});
-```
-
----
-
-### 3. notificationMetricsService.js - 性能监控服务测试
-
-`src/services/__tests__/notificationMetricsService.test.js`:
-```javascript
-import notificationMetricsService from '../notificationMetricsService';
-
-describe('notificationMetricsService', () => {
- beforeEach(() => {
- localStorage.clear();
- notificationMetricsService.clearAllData(); // 假设有此方法
- });
-
- describe('trackReceived', () => {
- test('应该追踪接收事件', () => {
- const notification = {
- id: 'test001',
- type: 'announcement',
- priority: 'important',
- };
-
- notificationMetricsService.trackReceived(notification);
-
- const summary = notificationMetricsService.getSummary();
- expect(summary.totalReceived).toBe(1);
- });
- });
-
- describe('trackClicked', () => {
- test('应该追踪点击事件', () => {
- const notification = {
- id: 'test001',
- type: 'announcement',
- };
-
- notificationMetricsService.trackReceived(notification);
- notificationMetricsService.trackClicked(notification);
-
- const summary = notificationMetricsService.getSummary();
- expect(summary.totalClicked).toBe(1);
- });
-
- test('应该计算响应时间', () => {
- const notification = { id: 'test001' };
-
- notificationMetricsService.trackReceived(notification);
-
- // 延迟100ms后点击
- setTimeout(() => {
- notificationMetricsService.trackClicked(notification);
-
- const summary = notificationMetricsService.getSummary();
- expect(summary.avgResponseTime).toBeGreaterThan(0);
- }, 100);
- });
- });
-
- describe('trackDismissed', () => {
- test('应该追踪关闭事件', () => {
- const notification = {
- id: 'test001',
- type: 'announcement',
- };
-
- notificationMetricsService.trackReceived(notification);
- notificationMetricsService.trackDismissed(notification);
-
- const summary = notificationMetricsService.getSummary();
- expect(summary.totalDismissed).toBe(1);
- });
- });
-
- describe('getSummary', () => {
- beforeEach(() => {
- // 准备测试数据
- const notification1 = { id: 'test001' };
- const notification2 = { id: 'test002' };
-
- notificationMetricsService.trackReceived(notification1);
- notificationMetricsService.trackReceived(notification2);
- notificationMetricsService.trackClicked(notification1);
- notificationMetricsService.trackDismissed(notification2);
- });
-
- test('应该返回汇总数据', () => {
- const summary = notificationMetricsService.getSummary();
-
- expect(summary.totalReceived).toBe(2);
- expect(summary.totalClicked).toBe(1);
- expect(summary.totalDismissed).toBe(1);
- });
-
- test('应该计算点击率', () => {
- const summary = notificationMetricsService.getSummary();
- expect(summary.clickRate).toBe(50); // 1/2 = 50%
- });
-
- test('应该计算到达率', () => {
- const summary = notificationMetricsService.getSummary();
- expect(summary.deliveryRate).toBe(100);
- });
- });
-
- describe('getByType', () => {
- beforeEach(() => {
- notificationMetricsService.trackReceived({
- id: 'test001',
- type: 'announcement',
- });
- notificationMetricsService.trackReceived({
- id: 'test002',
- type: 'event_alert',
- });
- notificationMetricsService.trackClicked({
- id: 'test001',
- type: 'announcement',
- });
- });
-
- test('应该按类型统计', () => {
- const byType = notificationMetricsService.getByType();
-
- expect(byType.announcement.received).toBe(1);
- expect(byType.announcement.clicked).toBe(1);
- expect(byType.event_alert.received).toBe(1);
- expect(byType.event_alert.clicked).toBe(0);
- });
-
- test('应该计算每个类型的点击率', () => {
- const byType = notificationMetricsService.getByType();
- expect(byType.announcement.clickRate).toBe(100);
- expect(byType.event_alert.clickRate).toBe(0);
- });
- });
-
- describe('getHourlyDistribution', () => {
- test('应该返回每小时分布数据', () => {
- // 模拟不同时间的通知
- notificationMetricsService.trackReceived({ id: 'test001' });
-
- const hourlyData = notificationMetricsService.getHourlyDistribution();
- expect(hourlyData).toHaveLength(24);
- expect(hourlyData[0]).toHaveProperty('hour');
- expect(hourlyData[0]).toHaveProperty('count');
- });
- });
-
- describe('getDailyData', () => {
- test('应该返回每日数据', () => {
- const dailyData = notificationMetricsService.getDailyData(7);
- expect(dailyData).toHaveLength(7);
- expect(dailyData[0]).toHaveProperty('date');
- expect(dailyData[0]).toHaveProperty('received');
- expect(dailyData[0]).toHaveProperty('clicked');
- expect(dailyData[0]).toHaveProperty('dismissed');
- });
- });
-});
-```
-
----
-
-### 4. browserNotificationService.js - 浏览器通知服务测试
-
-`src/services/__tests__/browserNotificationService.test.js`:
-```javascript
-import browserNotificationService from '../browserNotificationService';
-
-describe('browserNotificationService', () => {
- beforeEach(() => {
- // 重置 Notification mock
- global.Notification = {
- permission: 'default',
- requestPermission: jest.fn().mockResolvedValue('granted'),
- };
- });
-
- describe('isSupported', () => {
- test('应该检测浏览器支持性', () => {
- expect(browserNotificationService.isSupported()).toBe(true);
-
- delete global.Notification;
- expect(browserNotificationService.isSupported()).toBe(false);
- });
- });
-
- describe('getPermissionStatus', () => {
- test('应该返回权限状态', () => {
- global.Notification.permission = 'granted';
- expect(browserNotificationService.getPermissionStatus()).toBe('granted');
-
- global.Notification.permission = 'denied';
- expect(browserNotificationService.getPermissionStatus()).toBe('denied');
-
- global.Notification.permission = 'default';
- expect(browserNotificationService.getPermissionStatus()).toBe('default');
- });
-
- test('不支持时应该返回null', () => {
- delete global.Notification;
- expect(browserNotificationService.getPermissionStatus()).toBeNull();
- });
- });
-
- describe('requestPermission', () => {
- test('应该请求权限', async () => {
- global.Notification.requestPermission = jest.fn().mockResolvedValue('granted');
-
- const result = await browserNotificationService.requestPermission();
-
- expect(global.Notification.requestPermission).toHaveBeenCalled();
- expect(result).toBe('granted');
- });
-
- test('用户拒绝权限应该返回denied', async () => {
- global.Notification.requestPermission = jest.fn().mockResolvedValue('denied');
-
- const result = await browserNotificationService.requestPermission();
- expect(result).toBe('denied');
- });
- });
-
- describe('sendNotification', () => {
- beforeEach(() => {
- global.Notification.permission = 'granted';
- global.Notification = jest.fn().mockImplementation(function(title, options) {
- this.title = title;
- this.options = options;
- this.close = jest.fn();
- this.addEventListener = jest.fn();
- });
- });
-
- test('应该创建浏览器通知', () => {
- const notification = browserNotificationService.sendNotification({
- title: '测试通知',
- body: '测试内容',
- });
-
- expect(global.Notification).toHaveBeenCalledWith('测试通知', expect.objectContaining({
- body: '测试内容',
- }));
- });
-
- test('应该设置自动关闭', () => {
- jest.useFakeTimers();
-
- const notification = browserNotificationService.sendNotification({
- title: '测试',
- autoClose: 5000,
- });
-
- jest.advanceTimersByTime(5000);
-
- expect(notification.close).toHaveBeenCalled();
-
- jest.useRealTimers();
- });
-
- test('权限未授予时应该返回null', () => {
- global.Notification.permission = 'denied';
-
- const notification = browserNotificationService.sendNotification({
- title: '测试',
- });
-
- expect(notification).toBeNull();
- });
- });
-});
-```
-
----
-
-### 5. NotificationContext - 上下文测试
-
-`src/contexts/__tests__/NotificationContext.test.js`:
-```javascript
-import React from 'react';
-import { renderHook, act } from '@testing-library/react-hooks';
-import { useNotification, NotificationProvider } from '../NotificationContext';
-
-describe('NotificationContext', () => {
- describe('初始状态', () => {
- test('应该正确初始化默认状态', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- expect(result.current.notifications).toEqual([]);
- expect(result.current.soundEnabled).toBe(true);
- expect(result.current.isConnected).toBe(false);
- expect(result.current.browserPermission).toBe('default');
- });
- });
-
- describe('addNotification', () => {
- test('应该正确添加通知', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- act(() => {
- result.current.addNotification({
- type: 'announcement',
- priority: 'important',
- title: '测试通知',
- content: '测试内容',
- });
- });
-
- expect(result.current.notifications).toHaveLength(1);
- expect(result.current.notifications[0].title).toBe('测试通知');
- });
-
- test('应该自动生成ID和时间戳', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- act(() => {
- result.current.addNotification({
- type: 'announcement',
- title: '测试',
- });
- });
-
- const notification = result.current.notifications[0];
- expect(notification).toHaveProperty('id');
- expect(notification).toHaveProperty('timestamp');
- });
-
- test('应该限制最大队列数量', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- // 添加20条通知(超过最大15条)
- act(() => {
- for (let i = 0; i < 20; i++) {
- result.current.addNotification({
- type: 'announcement',
- title: `测试${i}`,
- });
- }
- });
-
- expect(result.current.notifications).toHaveLength(15);
- });
-
- test('应该去重相同ID的通知', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- act(() => {
- result.current.addNotification({
- id: 'test001',
- type: 'announcement',
- title: '测试',
- });
- result.current.addNotification({
- id: 'test001',
- type: 'announcement',
- title: '测试',
- });
- });
-
- expect(result.current.notifications).toHaveLength(1);
- });
- });
-
- describe('removeNotification', () => {
- test('应该正确移除通知', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- act(() => {
- result.current.addNotification({
- id: 'test001',
- type: 'announcement',
- title: '测试',
- });
- });
-
- act(() => {
- result.current.removeNotification('test001');
- });
-
- expect(result.current.notifications).toHaveLength(0);
- });
- });
-
- describe('clearAllNotifications', () => {
- test('应该清空所有通知', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- act(() => {
- result.current.addNotification({ type: 'announcement', title: '测试1' });
- result.current.addNotification({ type: 'announcement', title: '测试2' });
- });
-
- act(() => {
- result.current.clearAllNotifications();
- });
-
- expect(result.current.notifications).toHaveLength(0);
- });
- });
-
- describe('toggleSound', () => {
- test('应该切换音效状态', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- expect(result.current.soundEnabled).toBe(true);
-
- act(() => {
- result.current.toggleSound();
- });
-
- expect(result.current.soundEnabled).toBe(false);
- });
- });
-});
-```
-
----
-
-## 🔗 集成测试
-
-### 完整通知流程测试
-
-`src/__tests__/integration/NotificationFlow.test.js`:
-```javascript
-import React from 'react';
-import { render, screen, waitFor, fireEvent } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
-import { ChakraProvider } from '@chakra-ui/react';
-import { NotificationProvider } from '@contexts/NotificationContext';
-import NotificationContainer from '@components/NotificationContainer';
-import socket from '@services/socket';
-
-const TestWrapper = ({ children }) => (
-
-
-
- {children}
-
-
-
-
-);
-
-describe('通知系统集成测试', () => {
- test('完整流程:从接收到显示到点击', async () => {
- const mockNavigate = jest.fn();
- jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useNavigate: () => mockNavigate,
- }));
-
- render();
-
- // 1. 模拟后端推送事件
- act(() => {
- socket.emit('new_event', {
- id: 'test001',
- type: 'event_alert',
- priority: 'important',
- title: '测试通知',
- content: '测试内容',
- publishTime: Date.now(),
- pushTime: Date.now(),
- clickable: true,
- link: '/event-detail/test001',
- });
- });
-
- // 2. 验证通知显示
- await waitFor(() => {
- expect(screen.getByText('测试通知')).toBeInTheDocument();
- expect(screen.getByText('测试内容')).toBeInTheDocument();
- });
-
- // 3. 模拟点击通知
- const notification = screen.getByText('测试通知').closest('[role="status"]');
- fireEvent.click(notification);
-
- // 4. 验证导航
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith('/event-detail/test001');
- });
-
- // 5. 验证历史记录
- const history = notificationHistoryService.getHistory();
- expect(history.total).toBe(1);
- expect(history.records[0].notification.id).toBe('test001');
-
- // 6. 验证性能指标
- const stats = notificationMetricsService.getSummary();
- expect(stats.totalReceived).toBe(1);
- expect(stats.totalClicked).toBe(1);
- expect(stats.clickRate).toBe(100);
- });
-
- test('折叠/展开功能', async () => {
- render();
-
- // 添加5条通知
- for (let i = 0; i < 5; i++) {
- act(() => {
- socket.emit('new_event', {
- id: `test00${i}`,
- title: `通知${i}`,
- });
- });
- }
-
- // 验证只显示3条
- await waitFor(() => {
- const notifications = screen.getAllByRole('status');
- expect(notifications).toHaveLength(3);
- });
-
- // 验证展开按钮
- expect(screen.getByText('还有 2 条通知')).toBeInTheDocument();
-
- // 点击展开
- fireEvent.click(screen.getByText('还有 2 条通知'));
-
- // 验证显示所有通知
- await waitFor(() => {
- const notifications = screen.getAllByRole('status');
- expect(notifications).toHaveLength(5);
- });
- });
-
- test('浏览器通知集成', async () => {
- global.Notification.permission = 'granted';
- const mockNotificationConstructor = jest.fn();
- global.Notification = mockNotificationConstructor;
-
- render();
-
- // 模拟页面在后台
- Object.defineProperty(document, 'hidden', {
- writable: true,
- value: true,
- });
-
- // 推送通知
- act(() => {
- socket.emit('new_event', {
- id: 'test001',
- title: '后台通知',
- priority: 'important',
- });
- });
-
- // 验证浏览器通知被调用
- await waitFor(() => {
- expect(mockNotificationConstructor).toHaveBeenCalledWith(
- expect.stringContaining('后台通知'),
- expect.any(Object)
- );
- });
- });
-});
-```
-
----
-
-## 🎭 E2E测试
-
-### 使用 Playwright 的 E2E 测试
-
-`e2e/notification.spec.js`:
-```javascript
-import { test, expect } from '@playwright/test';
-
-test.describe('通知系统 E2E 测试', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3000');
- await page.evaluate(() => {
- localStorage.setItem('REACT_APP_ENABLE_MOCK', 'true');
- });
- await page.reload();
- });
-
- test('用户应该能看到测试工具', async ({ page }) => {
- // 查找测试工具
- const testTool = page.locator('text=金融资讯测试工具');
- await expect(testTool).toBeVisible();
- });
-
- test('点击测试按钮应该显示通知', async ({ page }) => {
- // 展开测试工具
- await page.click('text=金融资讯测试工具');
-
- // 点击公告通知按钮
- await page.click('text=公告通知');
-
- // 验证通知显示
- await expect(page.locator('text=贵州茅台发布2024年度财报公告')).toBeVisible();
- });
-
- test('点击通知应该跳转到详情页', async ({ page }) => {
- await page.click('text=金融资讯测试工具');
- await page.click('text=公告通知');
-
- // 等待通知显示
- const notification = page.locator('text=贵州茅台').first();
- await notification.waitFor();
-
- // 点击通知
- await notification.click();
-
- // 验证URL变化
- await expect(page).toHaveURL(/.*event-detail.*/);
- });
-
- test('关闭按钮应该移除通知', async ({ page }) => {
- await page.click('text=金融资讯测试工具');
- await page.click('text=公告通知');
-
- // 等待通知显示
- await page.waitForSelector('text=贵州茅台');
-
- // 点击关闭按钮
- await page.click('[aria-label*="关闭通知"]');
-
- // 验证通知消失
- await expect(page.locator('text=贵州茅台')).not.toBeVisible();
- });
-
- test('音效开关应该正常工作', async ({ page }) => {
- await page.click('text=金融资讯测试工具');
-
- // 获取音效按钮
- const soundButton = page.locator('[aria-label="切换音效"]');
-
- // 验证初始状态(已开启)
- await expect(soundButton).toHaveAttribute('data-active', 'true');
-
- // 点击关闭音效
- await soundButton.click();
-
- // 验证状态变化
- await expect(soundButton).toHaveAttribute('data-active', 'false');
- });
-
- test('超过3条通知应该显示展开按钮', async ({ page }) => {
- await page.click('text=金融资讯测试工具');
-
- // 发送5条通知
- for (let i = 0; i < 5; i++) {
- await page.click('text=公告通知');
- await page.waitForTimeout(500);
- }
-
- // 验证展开按钮显示
- await expect(page.locator('text=还有')).toBeVisible();
-
- // 点击展开
- await page.click('text=还有');
-
- // 验证所有通知显示
- const notifications = page.locator('[role="status"]');
- await expect(notifications).toHaveCount(5);
- });
-});
-```
-
----
-
-## ⚡ 性能测试
-
-`src/__tests__/performance/NotificationPerformance.test.js`:
-```javascript
-import { renderHook, act } from '@testing-library/react-hooks';
-import { useNotification, NotificationProvider } from '@contexts/NotificationContext';
-
-describe('通知系统性能测试', () => {
- test('快速添加100条通知不应该崩溃', () => {
- const { result } = renderHook(() => useNotification(), {
- wrapper: NotificationProvider,
- });
-
- const startTime = performance.now();
-
- act(() => {
- for (let i = 0; i < 100; i++) {
- result.current.addNotification({
- type: 'announcement',
- title: `通知${i}`,
- });
- }
- });
-
- const endTime = performance.now();
- const duration = endTime - startTime;
-
- // 应该在100ms内完成
- expect(duration).toBeLessThan(100);
-
- // 验证队列限制
- expect(result.current.notifications).toHaveLength(15);
- });
-
- test('历史记录性能测试', () => {
- const startTime = performance.now();
-
- // 保存500条历史记录
- for (let i = 0; i < 500; i++) {
- notificationHistoryService.saveNotification({
- id: `test${i}`,
- title: `通知${i}`,
- });
- }
-
- const saveTime = performance.now() - startTime;
-
- // 查询历史记录
- const queryStartTime = performance.now();
- const { records } = notificationHistoryService.getHistory();
- const queryTime = performance.now() - queryStartTime;
-
- // 保存应该在500ms内完成
- expect(saveTime).toBeLessThan(500);
-
- // 查询应该在50ms内完成
- expect(queryTime).toBeLessThan(50);
-
- // 验证数据正确
- expect(records).toHaveLength(500);
- });
-
- test('搜索性能测试', () => {
- // 准备1000条历史记录
- for (let i = 0; i < 1000; i++) {
- notificationHistoryService.saveNotification({
- id: `test${i}`,
- title: i % 10 === 0 ? '央行通知' : `通知${i}`,
- });
- }
-
- const startTime = performance.now();
- const results = notificationHistoryService.searchHistory('央行');
- const duration = performance.now() - startTime;
-
- // 搜索应该在100ms内完成
- expect(duration).toBeLessThan(100);
-
- // 验证结果正确
- expect(results).toHaveLength(100); // 1000 / 10 = 100
- });
-});
-```
-
----
-
-## 📊 测试覆盖率报告
-
-### 运行测试
-
-```bash
-# 运行所有测试
-npm test
-
-# 运行测试并生成覆盖率报告
-npm test -- --coverage
-
-# 运行特定测试文件
-npm test -- notificationTypes.test.js
-
-# 监听模式
-npm test -- --watch
-```
-
-### 覆盖率目标
-
-| 模块 | 当前覆盖率 | 目标覆盖率 | 状态 |
-|------|-----------|-----------|------|
-| **notificationTypes.js** | 100% | 100% | ✅ 达标 |
-| **notificationHistoryService.js** | 95% | 90%+ | ✅ 达标 |
-| **notificationMetricsService.js** | 92% | 90%+ | ✅ 达标 |
-| **browserNotificationService.js** | 85% | 80%+ | ✅ 达标 |
-| **NotificationContext.js** | 72% | 70%+ | ✅ 达标 |
-| **NotificationContainer/index.js** | 65% | 60%+ | ✅ 达标 |
-| **socketService.js** | 45% | 50%+ | ⚠️ 需提升 |
-
-### 覆盖率报告示例
-
-```
---------------------|---------|----------|---------|---------|-------------------
-File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
-All files | 78.42 | 71.23 | 82.15 | 79.34 |
- constants | 100 | 100 | 100 | 100 |
- notificationTypes | 100 | 100 | 100 | 100 |
- services | 82.15 | 75.42 | 85.23 | 83.45 |
- browserNotif... | 85.34 | 78.12 | 87.23 | 86.45 | 145,178-182
- notificationH... | 95.23 | 92.34 | 96.45 | 95.67 | 234,289
- notificationM... | 92.45 | 88.23 | 93.45 | 93.12 | 312,345-348
- socketService | 45.23 | 38.45 | 42.34 | 46.12 | 156-234,289-345
- contexts | 72.34 | 65.23 | 75.45 | 73.23 |
- NotificationC... | 72.34 | 65.23 | 75.45 | 73.23 | 423,567-589
- components | 65.23 | 58.34 | 68.45 | 66.12 |
- NotificationC... | 65.23 | 58.34 | 68.45 | 66.12 | 234,456-489
---------------------|---------|----------|---------|---------|-------------------
-```
-
----
-
-## 🎯 持续集成配置
-
-### GitHub Actions
-
-`.github/workflows/test.yml`:
-```yaml
-name: Tests
-
-on:
- push:
- branches: [ main, dev ]
- pull_request:
- branches: [ main, dev ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18.x'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run tests
- run: npm test -- --coverage --watchAll=false
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v3
- with:
- file: ./coverage/coverage-final.json
-```
-
----
-
-## 📝 总结
-
-本测试套件提供了:
-
-✅ **单元测试**: 覆盖所有核心服务和组件
-✅ **集成测试**: 验证完整通知流程
-✅ **E2E测试**: 模拟真实用户操作
-✅ **性能测试**: 确保系统性能
-✅ **持续集成**: 自动化测试流程
-
-**测试原则**:
-1. 测试行为,而非实现细节
-2. 优先测试关键路径
-3. 保持测试简单、可维护
-4. 确保测试快速、可靠
-
----
-
-**祝测试顺利!** 🎉