From de37546ddb206b49a4e5280df6993caaad864706 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 17 Nov 2025 15:14:24 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=88=A0=E9=99=A4=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 删除内容 - docs/TEST_GUIDE.md (7.4KB) - 崩溃修复测试指南 - docs/test-cases/notification-tests.md (49KB) - 自动化测试用例 - docs/test-cases/ 目录(已清空) ## 原因 - 这些文档是针对开发者的测试文档 - 通知测试工具(NotificationTestTool、window.__TEST_NOTIFICATION__)已删除 - 保留 NOTIFICATION_SYSTEM.md 作为主文档,后续可根据需要更新 ## 相关清理 已删除的测试工具: - NotificationTestTool 组件 - window.__TEST_NOTIFICATION__ API - notificationDebugger 调试工具 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/TEST_GUIDE.md | 338 ----- docs/test-cases/notification-tests.md | 1770 ------------------------- 2 files changed, 2108 deletions(-) delete mode 100644 docs/TEST_GUIDE.md delete mode 100644 docs/test-cases/notification-tests.md 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. 确保测试快速、可靠 - ---- - -**祝测试顺利!** 🎉