diff --git a/FRONTEND-TESTING.md b/FRONTEND-TESTING.md new file mode 100644 index 0000000..4c88163 --- /dev/null +++ b/FRONTEND-TESTING.md @@ -0,0 +1,367 @@ +# 前端测试指南 + +## 概述 + +本指南介绍如何对 Node WebSocket Chatroom 项目的前端进行测试,包括: +- 消息发送测试 +- 消息接收测试 +- 消息渲染测试 +- 在线列表更新测试 +- 样式展示测试 + +## 测试环境准备 + +### 1. 安装依赖 + +```bash +npm install +``` + +### 2. 启动开发服务器 + +```bash +npm run serve +``` + +### 3. 启动后端服务器 + +```bash +npm run prod +``` + +## 手动测试流程 + +### 测试一:消息发送功能 + +**测试步骤:** +1. 打开浏览器访问 http://localhost:8080 +2. 在登录页面输入用户名(如 "TestUser") +3. 选择一个头像 +4. 点击"登录"按钮 +5. 在左侧会话列表选择"群聊天室" +6. 在底部输入框输入消息内容(如 "Hello, World!") +7. 点击发送按钮或按回车键 + +**预期结果:** +- [ ] 消息立即显示在聊天区域右侧 +- [ ] 消息包含发送者头像和昵称 +- [ ] 消息时间戳正确显示 +- [ ] 输入框清空 + +**测试用例:** +| 测试项 | 输入 | 预期结果 | 实际结果 | +|--------|------|----------|----------| +| 普通文本 | "Hello World" | 正常发送并显示 | | +| 空消息 | "" | 不发送 | | +| 长文本(1000字) | 超长文本 | 正常发送,自动换行 | | +| 特殊字符 | "!@#$%^&*()" | 正常显示 | | +| 表情符号 | "😊👍🎉" | 正常显示 | | + +--- + +### 测试二:消息接收功能 + +**测试步骤:** +1. 打开两个浏览器窗口(或使用不同浏览器) +2. 在第一个窗口以 "User1" 登录 +3. 在第二个窗口以 "User2" 登录 +4. 在 User1 的窗口发送消息到群聊天室 +5. 观察 User2 的窗口 + +**预期结果:** +- [ ] User2 的聊天区域左侧显示 User1 发送的消息 +- [ ] 消息包含 User1 的头像和昵称 +- [ ] 消息时间戳正确 +- [ ] 如果 User2 不在当前会话,收到消息时应该有提示 +- [ ] 系统通知音播放(如果开启) + +**测试用例:** +| 测试场景 | 操作 | 预期结果 | 实际结果 | +|----------|------|----------|----------| +| 同时在线接收 | User1发送,User2在线 | User2实时收到 | | +| 离线后上线 | User1发送,User2上线后 | User2收到历史消息 | | +| 不同会话接收 | User1发送到群聊,User2在私聊 | User2收到群聊消息提示 | | + +--- + +### 测试三:消息渲染功能 + +**测试步骤:** +1. 登录系统 +2. 发送不同类型的消息 +3. 观察消息的渲染效果 + +**预期结果:** + +#### 文本消息 +- [ ] 文本正常显示 +- [ ] 换行符正确解析 +- [ ] 长文本自动换行 +- [ ] 表情符号正确渲染 + +#### 图片消息 +- [ ] 图片正确显示 +- [ ] 图片大小自适应 +- [ ] 点击图片可以查看大图(如果有此功能) + +#### 时间显示 +- [ ] 3分钟内显示"刚刚" +- [ ] 3分钟到1小时显示"X分钟前" +- [ ] 1小时到24小时显示"X小时前" +- [ ] 1天到3天显示"X天前" +- [ ] 超过3天显示具体日期 + +#### 气泡样式 +- [ ] 自己发送的消息气泡在右侧 +- [ ] 他人发送的消息气泡在左侧 +- [ ] 群聊消息显示发送者昵称 +- [ ] 私聊消息不显示昵称(如果设置关闭) + +--- + +### 测试四:在线列表更新 + +**测试步骤:** +1. 以 User1 登录 +2. 观察左侧用户列表 +3. 打开新窗口以 User2 登录 +4. 回到 User1 的窗口 +5. 在新窗口中 User2 退出登录 +6. 再次观察 User1 的窗口 + +**预期结果:** +- [ ] User1 登录后,列表中只有 User1 和群聊天室 +- [ ] User2 登录后,User1 的列表中实时添加 User2 +- [ ] 系统消息显示 "User2 加入了聊天室" +- [ ] User2 退出后,User1 的列表中移除 User2 +- [ ] 系统消息显示 "User2 离开了聊天室" + +**测试用例:** +| 操作 | 事件 | 列表变化 | 系统消息 | +|------|------|----------|----------| +| 用户登录 | join | 添加用户 | "XXX 加入了聊天室" | +| 用户退出 | logout | 移除用户 | "XXX 离开了聊天室" | +| 重复昵称 | loginFail | 无变化 | "登录失败,昵称已存在" | + +--- + +### 测试五:样式展示测试 + +**测试步骤:** +1. 在不同浏览器中打开应用 +2. 测试不同屏幕尺寸 +3. 测试深色/浅色主题(如果支持) + +**预期结果:** + +#### 响应式布局 +- [ ] PC端正常显示三栏布局 +- [ ] 移动端正常显示底部导航 +- [ ] 窗口大小变化时布局自适应 +- [ ] 滚动条样式统一 + +#### 颜色和样式 +- [ ] 主色调一致 +- [ ] 按钮 hover 效果正常 +- [ ] 输入框聚焦效果正常 +- [ ] 选中状态高亮正确 + +#### 字体和间距 +- [ ] 字体大小合适 +- [ ] 行间距合适 +- [ ] 内边距外边距一致 +- [ ] 对齐方式正确 + +--- + +### 测试六:特殊场景测试 + +**测试步骤:** +1. 测试网络波动情况 +2. 测试服务端重启 +3. 测试多标签页登录 + +**预期结果:** + +#### 断线重连 +- [ ] 网络断开时显示断线提示 +- [ ] 网络恢复后自动重连 +- [ ] 重连后保持登录状态 +- [ ] 重连后消息不丢失 + +#### 多标签页 +- [ ] 同一用户在多个标签页登录 +- [ ] 一个标签页发送消息,其他标签页能收到 +- [ ] 一个标签页退出,其他标签页也退出 + +--- + +## 自动化测试建议 + +### 使用 Cypress 进行端到端测试 + +```javascript +// cypress/e2e/chat-app.cy.js +describe('Chat App E2E Tests', () => { + beforeEach(() => { + cy.visit('http://localhost:8080'); + }); + + it('should login successfully', () => { + cy.get('.input-text').type('TestUser'); + cy.get('.form-btn').click(); + cy.contains('TestUser').should('be.visible'); + }); + + it('should send and receive messages', () => { + cy.get('.input-text').type('User1'); + cy.get('.form-btn').click(); + + cy.contains('群聊天室').click(); + cy.get('.message-input').type('Hello from Cypress'); + cy.get('.send-btn').click(); + + cy.contains('Hello from Cypress').should('be.visible'); + }); + + it('should show online users', () => { + cy.get('.input-text').type('OnlineTestUser'); + cy.get('.form-btn').click(); + + cy.contains('群聊天室').should('be.visible'); + cy.contains('OnlineTestUser').should('be.visible'); + }); +}); +``` + +### 使用 Vue Test Utils 进行组件测试 + +```javascript +// tests/unit/components/ChatApp.test.js +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import ChatApp from '@/components/ChatApp.vue'; +import { friendlyTime, formatTime } from '@/components/filters'; + +describe('ChatApp.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(ChatApp); + }); + + describe('Filters', () => { + it('should format time correctly', () => { + const timestamp = new Date(2023, 5, 15, 14, 30, 45).getTime(); + expect(formatTime(timestamp)).toBe('2023.6.15 14:30:45'); + }); + + it('should show friendly time', () => { + const now = Date.now(); + const twoMinutesAgo = now - 2 * 60 * 1000; + expect(friendlyTime(twoMinutesAgo)).toBe('刚刚'); + }); + }); + + describe('Methods', () => { + it('should add message to session', () => { + const message = { + from: { id: '1', name: 'Test' }, + to: { id: 'group_001', type: 'group' }, + content: 'Test message', + time: Date.now(), + type: 'text' + }; + + wrapper.vm.addSessionMessage(message, 'group_001'); + + const messages = wrapper.vm.getMessages('group_001'); + expect(messages.length).toBe(1); + expect(messages[0].content).toBe('Test message'); + }); + + it('should search users correctly', () => { + wrapper.setData({ + users: [ + { id: '1', name: 'Alice', type: 'user' }, + { id: '2', name: 'Bob', type: 'user' }, + { id: '3', name: 'Charlie', type: 'user' } + ] + }); + + const results = wrapper.vm.searchUser('Al'); + expect(results.length).toBe(1); + expect(results[0].name).toBe('Alice'); + }); + }); +}); +``` + +--- + +## 测试报告模板 + +### 测试执行日期:____年____月____日 + +### 测试人员:____________ + +### 测试环境: +- 浏览器: + - Chrome 版本: + - Firefox 版本: + - Safari 版本: + - Edge 版本: +- 操作系统: +- Node.js 版本: +- npm 版本: + +### 测试结果汇总: + +| 测试类别 | 用例总数 | 通过 | 失败 | 阻塞 | 通过率 | +|----------|----------|------|------|------|--------| +| 消息发送 | | | | | | +| 消息接收 | | | | | | +| 消息渲染 | | | | | | +| 在线列表 | | | | | | +| 样式展示 | | | | | | +| 异常场景 | | | | | | +| **合计** | | | | | | + +### 发现的问题: + +| 问题编号 | 问题描述 | 严重程度 | 状态 | +|----------|----------|----------|------| +| | | 高/中/低 | 待修复/已修复 | +| | | | | + +### 测试结论: + +□ 测试通过,可发布 +□ 存在阻塞问题,暂缓发布 +□ 需要进一步测试 + +--- + +## 运行命令汇总 + +```bash +# 安装依赖 +npm install + +# 启动开发服务器 +npm run serve + +# 启动后端服务器 +npm run prod + +# 运行所有后端测试 +npm run test:all + +# 只运行单元测试 +npm run test:quick + +# 运行覆盖率测试 +npm run test:coverage + +# 运行压力测试 +npm run test:performance +``` diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..e944b91 --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,146 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| server | +
+
+ |
+ 21.01% | +58/276 | +6.25% | +6/96 | +26.22% | +16/61 | +21.16% | +58/274 | +
| src | +
+
+ |
+ 0% | +0/25 | +100% | +0/0 | +0% | +0/6 | +0% | +0/25 | +
| src/components | +
+
+ |
+ 30.12% | +25/83 | +52.63% | +20/38 | +11.76% | +2/17 | +30.86% | +25/81 | +