From b3c857cf77cb1fb08f8240b00b02a765cf93fc85 Mon Sep 17 00:00:00 2001 From: Unizzr <1920158711@qq.com> Date: Sun, 19 Apr 2026 22:42:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Jest=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=A1=86=E6=9E=B6=E5=8F=8A=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test: 添加单元测试和前端测试 ci: 配置Jest覆盖率报告 docs: 添加前端测试指南文档 --- FRONTEND-TESTING.md | 367 +++++ coverage/lcov-report/base.css | 224 +++ coverage/lcov-report/block-navigation.js | 87 + coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 146 ++ coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + coverage/lcov-report/server/db.js.html | 103 ++ coverage/lcov-report/server/index.html | 191 +++ coverage/lcov-report/server/index.js.html | 130 ++ coverage/lcov-report/server/io.js.html | 463 ++++++ coverage/lcov-report/server/jwt.js.html | 136 ++ coverage/lcov-report/server/store.js.html | 286 ++++ coverage/lcov-report/server/utils.js.html | 1090 ++++++++++++ coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 210 +++ .../src/components/Message.js.html | 262 +++ .../lcov-report/src/components/config.js.html | 94 ++ .../lcov-report/src/components/emoji.js.html | 661 ++++++++ .../src/components/filters.js.html | 190 +++ .../src/components/haokan_emojis.js.html | 1177 +++++++++++++ .../lcov-report/src/components/index.html | 191 +++ .../src/components/xigua_emojis.js.html | 1462 +++++++++++++++++ coverage/lcov-report/src/index.html | 116 ++ coverage/lcov-report/src/main.js.html | 247 +++ coverage/lcov.info | 799 +++++++++ jest.config.js | 14 + package.json | 12 +- run-tests.js | 145 ++ tests/exception/exceptions.test.js | 333 ++++ tests/frontend/chat-logic.test.js | 437 +++++ tests/frontend/filters.test.js | 95 ++ tests/frontend/message-panel.test.js | 252 +++ tests/frontend/user-login.test.js | 141 ++ tests/integration/socket.test.js | 255 +++ tests/performance/performance.test.js | 337 ++++ tests/unit/filters.test.js | 74 + tests/unit/utils.test.js | 162 ++ 38 files changed, 10891 insertions(+), 1 deletion(-) create mode 100644 FRONTEND-TESTING.md create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/server/db.js.html create mode 100644 coverage/lcov-report/server/index.html create mode 100644 coverage/lcov-report/server/index.js.html create mode 100644 coverage/lcov-report/server/io.js.html create mode 100644 coverage/lcov-report/server/jwt.js.html create mode 100644 coverage/lcov-report/server/store.js.html create mode 100644 coverage/lcov-report/server/utils.js.html create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov-report/src/components/Message.js.html create mode 100644 coverage/lcov-report/src/components/config.js.html create mode 100644 coverage/lcov-report/src/components/emoji.js.html create mode 100644 coverage/lcov-report/src/components/filters.js.html create mode 100644 coverage/lcov-report/src/components/haokan_emojis.js.html create mode 100644 coverage/lcov-report/src/components/index.html create mode 100644 coverage/lcov-report/src/components/xigua_emojis.js.html create mode 100644 coverage/lcov-report/src/index.html create mode 100644 coverage/lcov-report/src/main.js.html create mode 100644 coverage/lcov.info create mode 100644 jest.config.js create mode 100644 run-tests.js create mode 100644 tests/exception/exceptions.test.js create mode 100644 tests/frontend/chat-logic.test.js create mode 100644 tests/frontend/filters.test.js create mode 100644 tests/frontend/message-panel.test.js create mode 100644 tests/frontend/user-login.test.js create mode 100644 tests/integration/socket.test.js create mode 100644 tests/performance/performance.test.js create mode 100644 tests/unit/filters.test.js create mode 100644 tests/unit/utils.test.js 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 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 21.61% + Statements + 83/384 +
+ + +
+ 19.4% + Branches + 26/134 +
+ + +
+ 21.42% + Functions + 18/84 +
+ + +
+ 21.84% + Lines + 83/380 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
server +
+
21.01%58/2766.25%6/9626.22%16/6121.16%58/274
src +
+
0%0/25100%0/00%0/60%0/25
src/components +
+
30.12%25/8352.63%20/3811.76%2/1730.86%25/81
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/server/db.js.html b/coverage/lcov-report/server/db.js.html new file mode 100644 index 0000000..91fecae --- /dev/null +++ b/coverage/lcov-report/server/db.js.html @@ -0,0 +1,103 @@ + + + + + + Code coverage report for server/db.js + + + + + + + + + +
+
+

All files / server db.js

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7  +  +  +  +  +  + 
const Datastore = require('nedb');
+const db={
+  user:new Datastore({filename:"./db/users.db",autoload:true}),
+  message:new Datastore({filename:"./db/messages.db",autoload:true})
+}
+module.exports=db;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/index.html b/coverage/lcov-report/server/index.html new file mode 100644 index 0000000..86cf9c3 --- /dev/null +++ b/coverage/lcov-report/server/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for server + + + + + + + + + +
+
+

All files server

+
+ +
+ 21.01% + Statements + 58/276 +
+ + +
+ 6.25% + Branches + 6/96 +
+ + +
+ 26.22% + Functions + 16/61 +
+ + +
+ 21.16% + Lines + 58/274 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
db.js +
+
0%0/3100%0/0100%0/00%0/3
index.js +
+
0%0/13100%0/00%0/10%0/13
io.js +
+
0%0/730%0/340%0/110%0/72
jwt.js +
+
57.14%4/7100%0/050%1/257.14%4/7
store.js +
+
0%0/340%0/120%0/120%0/34
utils.js +
+
36.98%54/14612%6/5042.85%15/3537.24%54/145
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/index.js.html b/coverage/lcov-report/server/index.js.html new file mode 100644 index 0000000..e6c572c --- /dev/null +++ b/coverage/lcov-report/server/index.js.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for server/index.js + + + + + + + + + +
+
+

All files / server index.js

+
+ +
+ 0% + Statements + 0/13 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/13 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const io=require("./io");
+const {getNetworkIPv4}=require("./utils");
+const express = require('express');
+const app = express();
+const server = require('http').createServer(app);
+app.use("/",express.static('dist'));
+app.use("/assets/images",express.static('upload'));
+const PORT=3000;
+io.attach(server);
+//启动服务器
+server.listen(PORT,()=> {
+  const address=getNetworkIPv4().address;
+  console.info("- Local:   http://localhost:"+PORT);
+  console.info(`- Network: http://${address}:`+PORT)
+});
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/io.js.html b/coverage/lcov-report/server/io.js.html new file mode 100644 index 0000000..2c4e130 --- /dev/null +++ b/coverage/lcov-report/server/io.js.html @@ -0,0 +1,463 @@ + + + + + + Code coverage report for server/io.js + + + + + + + + + +
+
+

All files / server io.js

+
+ +
+ 0% + Statements + 0/73 +
+ + +
+ 0% + Branches + 0/34 +
+ + +
+ 0% + Functions + 0/11 +
+ + +
+ 0% + Lines + 0/72 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const io = require('socket.io')({
+  cors: {
+    origin: '*',
+    allowedHeaders: ["my-custom-header"],
+    credentials: true,
+  },
+  serveClient:false
+});
+const jwt=require("./jwt");
+const store=require("./store");
+const util={
+  async login(user,socket,isReconnect) {
+    let ip=socket.handshake.address.replace(/::ffff:/,"");
+    const headers = socket.handshake.headers;
+    const realIP = headers['x-forwarded-for'];
+    ip=realIP?realIP:ip;
+    const deviceType=this.getDeviceType(socket.handshake.headers["user-agent"].toLowerCase());
+    user.ip=ip;
+    user.deviceType=deviceType;
+    user.roomId=socket.id;
+    user.type='user';
+    if(isReconnect){
+      this.loginSuccess(user,socket);
+      console.log(`用户<${user.name}>重新链接成功!`)
+    }else {
+      const flag=await this.isHaveName(user.name);
+      if(!flag){
+        user.id=socket.id;
+        user.time=new Date().getTime();
+        this.loginSuccess(user,socket);
+        store.saveUser(user,'login')
+        const messages = await store.getMessages();
+        socket.emit("history-message","group_001",messages);
+      }else {
+        console.log(`登录失败,昵称<${user.name}>已存在!`)
+        socket.emit('loginFail','登录失败,昵称已存在!')
+      }
+    }
+  },
+  async loginSuccess(user, socket) {
+    const data={
+      user:user,
+      token:jwt.token(user)
+    };
+    socket.broadcast.emit('system', user, 'join');
+    socket.on('message',(from, to,message,type)=> {
+      if(to.type==='user'){
+        socket.broadcast.to(to.roomId).emit('message', socket.user, to,message,type);
+      }
+      if(to.type==='group'){
+        socket.broadcast.emit('message', socket.user,to,message,type);
+        store.saveMessage(from,to,message,type)
+      }
+    });
+    const users=await this.getOnlineUsers();
+    socket.user=user;
+    socket.emit('loginSuccess', data, users);
+  },
+  //根据useragent判读设备类型
+  getDeviceType(userAgent){
+    let bIsIpad = userAgent.match(/ipad/i) == "ipad";
+    let bIsIphoneOs = userAgent.match(/iphone os/i) == "iphone os";
+    let bIsMidp = userAgent.match(/midp/i) == "midp";
+    let bIsUc7 = userAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4";
+    let bIsUc = userAgent.match(/ucweb/i) == "ucweb";
+    let bIsAndroid = userAgent.match(/android/i) == "android";
+    let bIsCE = userAgent.match(/windows ce/i) == "windows ce";
+    let bIsWM = userAgent.match(/windows mobile/i) == "windows mobile";
+    if (bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM) {
+      return "phone";
+    } else {
+      return "pc";
+    }
+  },
+  //获取在线用户列表
+  async getOnlineUsers(){
+    const users=[
+      {
+        id:"group_001",
+        name:"群聊天室",
+        avatarUrl:"static/img/avatar/group-icon.png",
+        type:"group"
+      }
+    ];
+    const clients=await io.fetchSockets();
+    clients.forEach((item) => {
+      if(item.user){
+        users.push(item.user)
+      }
+    })
+    return users;
+  },
+  //判断用户是否已经存在
+  async isHaveName(name){
+    const users=await this.getOnlineUsers();
+    return users.some(item => item.name===name)
+  },
+};
+io.sockets.on('connection',(socket)=>{
+  const token=socket.handshake.headers.token;
+  let decode=null;
+  if(token){
+    decode=jwt.decode(token);
+  }
+  let user=decode?decode.data:{};
+  socket.on("disconnect",(reason)=>{
+    //判断是否是已登录用户
+    if (socket.user&&socket.user.id) {
+      //删除登录用户信息,并通知所有在线用户
+      socket.broadcast.emit('system', socket.user, 'logout');
+      store.saveUser(socket.user,'logout')
+    }
+    console.log(reason)
+  });
+  //判断链接用户是否已经登录
+  if(user&&user.id){
+    //已登录的用户重新登录
+    util.login(user,socket,true);
+  }else {
+    //监听用户登录事件
+    socket.on('login',(user)=>{
+      util.login(user,socket,false)
+    });
+  }
+});
+module.exports=io;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/jwt.js.html b/coverage/lcov-report/server/jwt.js.html new file mode 100644 index 0000000..e95215d --- /dev/null +++ b/coverage/lcov-report/server/jwt.js.html @@ -0,0 +1,136 @@ + + + + + + Code coverage report for server/jwt.js + + + + + + + + + +
+
+

All files / server jwt.js

+
+ +
+ 57.14% + Statements + 4/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 57.14% + Lines + 4/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +183x +3x +  +  +120x +  +  +  +  +  +  +  +  +  +  +  +3x + 
const JWT=require("jsonwebtoken");
+const auth={
+  secret:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
+  token(data){
+    return JWT.sign({data},this.secret,{
+      expiresIn:"1d"
+    })
+  },
+  decode(token){
+    try {
+      return JWT.verify(token,this.secret);
+    }catch (e) {
+      return null
+    }
+  }
+};
+module.exports=auth;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/store.js.html b/coverage/lcov-report/server/store.js.html new file mode 100644 index 0000000..e191955 --- /dev/null +++ b/coverage/lcov-report/server/store.js.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for server/store.js + + + + + + + + + +
+
+

All files / server store.js

+
+ +
+ 0% + Statements + 0/34 +
+ + +
+ 0% + Branches + 0/12 +
+ + +
+ 0% + Functions + 0/12 +
+ + +
+ 0% + Lines + 0/34 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const db=require("./db");
+const util=require("./utils")
+const fs=require('fs')
+module.exports ={
+  saveUser(user,status){
+    console.log(user.name,status);
+    if(status==='login'){
+      return new Promise((resolve, reject) => {
+        db.user.insert(user,(err,newUser) => {
+          if(err){
+            reject(err)
+          }else {
+            resolve(newUser)
+          }
+        })
+      })
+    }
+  },
+  saveMessage(from,to,message,type){
+    if(type==='image'){
+      const base64Data = message.replace(/^data:image\/\w+;base64,/, "")
+      const dataBuffer = new Buffer.from(base64Data,'base64')
+      const filename = util.MD5(base64Data)
+      fs.writeFileSync(`./upload/${filename}.png`,dataBuffer)
+      message=`/assets/images/${filename}.png`
+    }
+    console.log("\033[36m"+from.name+"\033[0m对<\033[36m"+to.name+"\033[0m>:\033[32m"+message+"\033[0m")
+    const doc={
+      from,
+      to,
+      content:message,
+      type,
+      time:new Date().getTime()
+    }
+    return new Promise((resolve, reject) => {
+      db.message.insert(doc,(err,newDoc) => {
+        if(err){
+          reject(err)
+        }else {
+          resolve(newDoc)
+        }
+      })
+    })
+  },
+  getMessages() {
+    return new Promise((resolve, reject) => {
+      db.message.find({}).sort({time:1}).skip(0).limit(100).exec((err,docs) => {
+        if(err){
+          reject(err)
+        }else {
+          resolve(docs)
+        }
+      })
+    })
+  },
+  getUsers(){
+    return new Promise((resolve, reject) => {
+      db.user.find({}).sort({time:1}).skip(0).limit(100).exec((err,docs) => {
+        if(err){
+          reject(err)
+        }else {
+          resolve(docs)
+        }
+      })
+    })
+  }
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/server/utils.js.html b/coverage/lcov-report/server/utils.js.html new file mode 100644 index 0000000..18c0c1b --- /dev/null +++ b/coverage/lcov-report/server/utils.js.html @@ -0,0 +1,1090 @@ + + + + + + Code coverage report for server/utils.js + + + + + + + + + +
+
+

All files / server utils.js

+
+ +
+ 36.98% + Statements + 54/146 +
+ + +
+ 12% + Branches + 6/50 +
+ + +
+ 42.85% + Functions + 15/35 +
+ + +
+ 37.24% + Lines + 54/145 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +3361x +1x +1x +  +  +  +  +  +1x +  +24x +  +3x +  +  +  +  +  +  +1x +5x +5x +5x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +4x +  +1x +4x +4x +3x +2x +  +3x +  +1x +  +  +1x +  +  +1x +3x +3x +  +  +  +1x +3x +3x +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +2x +  +1x +6x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const path=require("path");
+const fs=require("fs");
+const crypto = require('crypto');
+/**
+ * @Description:生成全局唯一id
+ * @Author:qinzhen
+ * @Date:2021-06-22
+*/
+const guid=()=> {
+  function S4() {
+    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
+  }
+  return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
+};
+/**
+ * @Description:md5值计算
+ * @Author:qinzhen
+ * @Date:2021-06-17
+*/
+const MD5=(text)=>{
+  let m = crypto.createHash('md5');
+  m.update(text, 'utf8');
+  return m.digest('hex').toLowerCase();
+};
+/**
+ * @Description:获取文件夹目录
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const getFolderContents=(folder,fPath)=>{
+  const distPath=path.join(fPath,folder);
+  const isExist=fs.existsSync(distPath);
+  if(!isExist){
+    console.error('文件夹路径错误');
+    return null;
+  }
+  let stat = fs.statSync(distPath);
+  if(stat.isDirectory()){
+    const contents=[];
+    const files = fs.readdirSync(distPath);
+    files.forEach((item)=>{
+      const itemPath=path.join(fPath,folder,item);
+      const fileStat=fs.statSync(itemPath);
+      const file={
+        id:MD5(itemPath),
+        name:item,
+        type:"file",
+        path:itemPath,
+        birthtime:fileStat.birthtime,
+        mtime:fileStat.mtime
+      };
+      if(fileStat.isDirectory()){
+        file.type='folder';
+      }else {
+        file.size=fileStat.size;
+      }
+      contents.push(file)
+    });
+    return contents;
+  }else {
+    console.error("该路径不是一个文件夹")
+    return null;
+  }
+};
+/**
+ * @Description:获取目录树结构
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const getFolderTree=(folder,fPath,deep=0)=>{
+  const distPath=path.join(fPath,folder);
+  const isExist=fs.existsSync(distPath);
+  if(!isExist){
+    return [];
+  }
+  const files = fs.readdirSync(distPath);
+  const arr=[];
+  files.forEach((item)=>{
+    let curPath = path.join(fPath,folder,item);
+    let itemPath=path.join(folder,item);
+    let stat = fs.statSync(curPath);
+    let file={
+      id:MD5(curPath),
+      name:item,
+      type:"file",
+      path:itemPath,
+      birthtime:stat.birthtime,
+      mtime:stat.mtime,
+      deep:deep
+    };
+    if(stat.isDirectory()){
+      file.type='folder';
+      file.children=getFolderTree(file.path,fPath,deep+1);
+      file.isOpen=false;
+    }else {
+      file.size=stat.size;
+    }
+    arr.push(file)
+  });
+  return arr.sort((a,b)=>{
+    if(a.type==='folder'&&b.type==='folder'){
+      return 0
+    }
+    if(a.type!=='folder'&&b.type==='folder'){
+      return 1
+    }
+    if(a.type==='folder'&&b.type!=='folder'){
+      return -1
+    }
+    return 0
+  });
+};
+/**
+ * @Description:遍历文件目录树
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const forEachTree= (trees,fn,deep=0)=>{
+  trees.forEach((node)=>{
+    fn(node,deep);
+    if(node.children&&node.children.length>0){
+      forEachTree(node.children,fn,deep+1)
+    }
+  })
+}
+/**
+ * @Description:打印目录树结构
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const logTree=(trees)=>{
+  forEachTree(trees,(node,deep)=>{
+    let pre="├─";
+    for (let i = 0; i < deep; i++) {
+      if(i<(deep-1)){
+        pre+="| ";
+      }else {
+        pre+="├-";
+      }
+    }
+    if(deep===0){
+      pre="├─"
+    }
+    console.log(pre+node.name);
+  });
+};
+/**
+ * @Description:map遍历目录树返会新的目录树
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const mapTree=(trees,fn,deep=0) => {
+  return trees.map((node) => {
+    let item=fn(node,deep);
+    if(node.children&&node.children.length>0){
+      item.children=mapTree(node.children,fn,deep+1)
+    }
+    return item;
+  })
+};
+/**
+ * @Description:过滤目录树结构返回新的目录树
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const filterTree=(trees,fn,deep=0) => {
+  return trees.filter((node) => {
+    if(node.children&&node.children.length>0){
+      let list=filterTree(node.children,fn,deep+1);
+      if(list.length>0){
+        return true
+      }else {
+        node.children=list;
+        return fn(node,deep)
+      }
+    }else {
+      return fn(node,deep);
+    }
+  })
+};
+/**
+ * @Description:生成公钥和私钥
+ * @Author:qinzhen
+ * @Date:2021-06-10
+*/
+const generateKey=() => {
+  const generate=crypto.generateKeyPairSync('rsa', {
+    modulusLength: 2048,
+    publicKeyEncoding: {
+      type: 'spki',
+      format: 'pem'
+    },
+    privateKeyEncoding: {
+      type: 'pkcs8',
+      format: 'pem'
+    }
+  });
+  return {
+    publicKey:generate.publicKey.toString('ascii'),
+    privateKey:generate.privateKey.toString('ascii')
+  }
+};
+/**
+ * @Description:公钥加密
+ * @Author:qinzhen
+ * @Date:2021-03-31
+ */
+const pubEncrypt=(publicKey,text)=>{
+  return crypto.publicEncrypt(publicKey, Buffer.from(text)).toString('base64');
+};
+/**
+ * @Description:私钥解密
+ * @Author:qinzhen
+ * @Date:2021-03-31
+ */
+const priDecrypt=(privateKey,ciphertext)=>{
+  return crypto.privateDecrypt(privateKey, Buffer.from(ciphertext.toString('base64'), 'base64'));
+};
+/**
+ * @Description:AES192解密算法
+ * @Author:qinzhen
+ * @Date:2021-03-31
+ */
+const decrypt=function(encrypted,key){
+  const decipher = crypto.createDecipher('aes192', key);
+  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
+  decrypted += decipher.final('utf8');
+  return decrypted;
+};
+/**
+ * @Description:AES192加密算法
+ * @Author:qinzhen
+ * @Date:2021-03-31
+ */
+const encrypt=function(data,key){
+  const cipher = crypto.createCipher('aes192', key);
+  let crypted = cipher.update(data, 'utf8', 'hex');
+  crypted += cipher.final('hex');
+  return crypted;
+};
+/**
+ * @Description:获取文件的hash值
+ * @Author:qinzhen
+ * @Date:2021-05-21
+ */
+const getFileHash=(filePath)=> {
+  return new Promise((resolve, reject) => {
+    let stream=fs.createReadStream(filePath);
+    let fsHash=crypto.createHash("md5");
+    stream.on("data",(d)=>{
+      fsHash.update(d);
+    })
+    stream.on('error',(err)=>{
+      reject(err)
+      console.log(err);
+    })
+    stream.on("end",()=>{
+      let md5=fsHash.digest("hex");
+      console.log(md5)
+      resolve(md5)
+    })
+  })
+};
+/**
+ * @Description:id与pid的关系列表生成树形结构数据
+ * @Author:qinzhen
+ * @Date:2021-06-22
+*/
+const generateTree=(items) => {
+  let map={};
+  let result=[];
+  items.forEach((item) => {
+    map[item.id]=item;
+  });
+  items.forEach((item) => {
+    const parent=map[item.pid];
+    if(parent){
+      if(!parent.children){
+        parent.children=[];
+      }
+      parent.children.push(item)
+    }else {
+      result.push(item)
+    }
+  });
+  return result;
+};
+// 下划线转换驼峰
+const toHump=(name)=> {
+  return name.replace(/_(\w)/g, function(all, letter){
+    return letter.toUpperCase();
+  });
+}
+//下划线转驼峰
+const toLine=(name)=>{
+  return name.replace(/([A-Z])/g,(all,letter) => {
+    return '_'+letter.toLowerCase();
+  })
+}
+/**
+ * @Description:获取计算机外部IPv4地址信息
+ * @Author:qinzhen
+ * @Date:2021-03-31
+ */
+const getNetworkIPv4=()=>{
+  const os = require("os");
+  const network=os.networkInterfaces();
+  let ips=[];
+  for (const key in network) {
+    ips=ips.concat(network[key])
+  }
+  return ips.filter((item)=>{
+    return item.family==='IPv4'&&!item.internal;
+  })[0]
+};
+module.exports={
+  guid,
+  MD5,
+  toHump,
+  toLine,
+  getFolderContents,
+  getFolderTree,
+  forEachTree,
+  logTree,
+  mapTree,
+  filterTree,
+  generateKey,
+  pubEncrypt,
+  priDecrypt,
+  decrypt,
+  encrypt,
+  getFileHash,
+  generateTree,
+  getNetworkIPv4
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/src/components/Message.js.html b/coverage/lcov-report/src/components/Message.js.html new file mode 100644 index 0000000..93838f2 --- /dev/null +++ b/coverage/lcov-report/src/components/Message.js.html @@ -0,0 +1,262 @@ + + + + + + Code coverage report for src/components/Message.js + + + + + + + + + +
+
+

All files / src/components Message.js

+
+ +
+ 0% + Statements + 0/16 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Vue from "vue";
+function Notice(msg,type) {
+  let Instance=Vue.extend({
+    render(h){
+      return h('transition',{
+        props:{
+          name:'dialog-slide'
+        },
+        on:{
+          afterLeave:this.delELe
+        }
+      },[h('div',{
+        class:`alter-message alter-message-${type}`,
+        directives:[
+          {
+            name:'show',
+            value:this.show
+          }
+        ]
+      },msg)])
+    },
+    data(){
+      return {
+        show:false
+      }
+    },
+    mounted(){
+      let _this=this;
+      this.show=true;
+      this.inter=setTimeout(function () {
+        _this.show=false;
+      },3000)
+    },
+    methods:{
+      delELe:function () {
+        if(this.inter){
+          clearTimeout(this.inter)
+        }
+        this.$el.parentNode.removeChild(this.$el);
+      }
+    }
+  });
+  document.body.appendChild(new Instance().$mount().$el)
+  return Instance;
+}
+export default {
+  info(text){
+    Notice(text,'info');
+  },
+  error(text){
+    Notice(text,'error');
+  },
+  success(text) {
+    Notice(text,'success');
+  },
+  warning(text){
+    Notice(text,'warning');
+  }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/config.js.html b/coverage/lcov-report/src/components/config.js.html new file mode 100644 index 0000000..2bd4ea4 --- /dev/null +++ b/coverage/lcov-report/src/components/config.js.html @@ -0,0 +1,94 @@ + + + + + + Code coverage report for src/components/config.js + + + + + + + + + +
+
+

All files / src/components config.js

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4  +  +  + 
export const BASE_URL = process.env.BASE_URL
+export const BELL_URL = BASE_URL+'static/8400.mp3'
+export const EMOJI_BASE_URL = BASE_URL+'static/img/emoji'
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/emoji.js.html b/coverage/lcov-report/src/components/emoji.js.html new file mode 100644 index 0000000..dd64291 --- /dev/null +++ b/coverage/lcov-report/src/components/emoji.js.html @@ -0,0 +1,661 @@ + + + + + + Code coverage report for src/components/emoji.js + + + + + + + + + +
+
+

All files / src/components emoji.js

+
+ +
+ 0% + Statements + 0/35 +
+ + +
+ 0% + Branches + 0/16 +
+ + +
+ 0% + Functions + 0/5 +
+ + +
+ 0% + Lines + 0/33 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export const expressions=[
+  {title:"[微笑]",url: "/微笑.gif"},
+  {title:"[撇嘴]",url: "/撇嘴.gif"},
+  {title:"[色]",url: "/色.gif"},
+  {title:"[发呆]",url: "/发呆.gif"},
+  {title:"[得意]",url: "/得意.gif"},
+  {title:"[流泪]",url: "/流泪.gif"},
+  {title:"[害羞]",url: "/害羞.gif"},
+  {title:"[闭嘴]",url: "/闭嘴.gif"},
+  {title:"[睡]",url: "/睡.gif"},
+  {title:"[大哭]",url: "/大哭.gif"},
+  {title:"[尴尬]",url: "/尴尬.gif"},
+  {title:"[呲牙]",url: "/呲牙.gif"},
+  {title:"[发怒]",url: "/发怒.gif"},
+  {title:"[调皮]",url: "/调皮.gif"},
+  {title:"[惊讶]",url: "/惊讶.gif"},
+  {title:"[难过]",url: "/难过.gif"},
+  {title:"[酷]",url: "/酷.gif"},
+  {title:"[冷汗]",url: "/冷汗.gif"},
+  {title:"[抓狂]",url: "/抓狂.gif"},
+  {title:"[吐]",url: "/吐.gif"},
+  {title:"[偷笑]",url: "/偷笑.gif"},
+  {title:"[可爱]",url: "/可爱.gif"},
+  {title:"[白眼]",url: "/白眼.gif"},
+  {title:"[傲慢]",url: "/傲慢.gif"},
+  {title:"[饥饿]",url: "/饥饿.gif"},
+  {title:"[困]",url: "/困.gif"},
+  {title:"[惊恐]",url: "/惊恐.gif"},
+  {title:"[流汗]",url: "/流汗.gif"},
+  {title:"[憨笑]",url: "/憨笑.gif"},
+  {title:"[大兵]",url: "/大兵.gif"},
+  {title:"[奋斗]",url: "/奋斗.gif"},
+  {title:"[咒骂]",url: "/咒骂.gif"},
+  {title:"[疑问]",url: "/疑问.gif"},
+  {title:"[嘘]",url: "/嘘.gif"},
+  {title:"[晕]",url: "/晕.gif"},
+  {title:"[折磨]",url: "/折磨.gif"},
+  {title:"[衰]",url: "/衰.gif"},
+  {title:"[骷髅]",url: "/骷髅.gif"},
+  {title:"[敲打]",url: "/敲打.gif"},
+  {title:"[再见]",url: "/再见.gif"},
+  {title:"[擦汗]",url: "/擦汗.gif"},
+  {title:"[抠鼻]",url: "/抠鼻.gif"},
+  {title:"[鼓掌]",url: "/鼓掌.gif"},
+  {title:"[嗅大了]",url: "/嗅大了.gif"},
+  {title:"[坏笑]",url: "/坏笑.gif"},
+  {title:"[左哼哼]",url: "/左哼哼.gif"},
+  {title:"[右哼哼]",url: "/右哼哼.gif"},
+  {title:"[哈欠]",url: "/哈欠.gif"},
+  {title:"[鄙视]",url: "/鄙视.gif"},
+  {title:"[委屈]",url: "/委屈.gif"},
+  {title:"[可怜]",url: "/可怜.gif"},
+  {title:"[阴险]",url: "/阴险.gif"},
+  {title:"[亲亲]",url: "/亲亲.gif"},
+  {title:"[吓]",url: "/吓.gif"},
+  {title:"[快哭了]",url: "/快哭了.gif"},
+  {title:"[菜刀]",url: "/菜刀.gif"},
+  {title:"[西瓜]",url: "/西瓜.gif"},
+  {title:"[啤酒]",url: "/啤酒.gif"},
+  {title:"[篮球]",url: "/篮球.gif"},
+  {title:"[乒乓]",url: "/乒乓.gif"},
+  {title:"[咖啡]",url: "/咖啡.gif"},
+  {title:"[饭]",url: "/饭.gif"},
+  {title:"[猪头]",url: "/猪头.gif"},
+  {title:"[玫瑰]",url: "/玫瑰.gif"},
+  {title:"[凋谢]",url: "/凋谢.gif"},
+  {title:"[心]",url: "/心.gif"},
+  {title:"[心碎]",url: "/心碎.gif"},
+  {title:"[蛋糕]",url: "/蛋糕.gif"},
+  {title:"[闪电]",url: "/闪电.gif"},
+  {title:"[炸弹]",url: "/炸弹.gif"},
+  {title:"[刀]",url: "/刀.gif"},
+  {title:"[足球]",url: "/足球.gif"},
+  {title:"[瓢虫]",url: "/瓢虫.gif"},
+  {title:"[便便]",url: "/便便.gif"},
+  {title:"[夜晚]",url: "/夜晚.gif"},
+  {title:"[太阳]",url: "/太阳.gif"},
+  {title:"[礼物]",url: "/礼物.gif"},
+  {title:"[拥抱]",url: "/拥抱.gif"},
+  {title:"[强]",url: "/强.gif"},
+  {title:"[弱]",url: "/弱.gif"},
+  {title:"[握手]",url: "/握手.gif"},
+  {title:"[胜利]",url: "/胜利.gif"},
+  {title:"[抱拳]",url: "/抱拳.gif"},
+  {title:"[勾引]",url: "/勾引.gif"},
+  {title:"[拳头]",url: "/拳头.gif"},
+  {title:"[差劲]",url: "/差劲.gif"},
+  {title:"[爱你]",url: "/爱你.gif"},
+  {title:"[NO]",url: "/NO.gif"},
+  {title:"[OK]",url: "/OK.gif"},
+  {title:"[爱情]",url: "/爱情.gif"},
+  {title:"[飞吻]",url: "/飞吻.gif"},
+  {title:"[发财]",url: "/发财.gif"},
+  {title:"[帅]",url: "/帅.gif"},
+  {title:"[雨伞]",url: "/雨伞.gif"},
+  {title:"[高铁左车头]",url: "/高铁左车头.gif"},
+  {title:"[车厢]",url: "/车厢.gif"},
+  {title:"[高铁右车头]",url: "/高铁右车头.gif"},
+  {title:"[纸巾]",url: "/纸巾.gif"},
+  {title:"[右太极]",url: "/右太极.gif"},
+  {title:"[左太极]",url: "/左太极.gif"},
+  {title:"[献吻]",url: "/献吻.gif"},
+  {title:"[街舞]",url: "/街舞.gif"},
+  {title:"[激动]",url: "/激动.gif"},
+  {title:"[挥动]",url: "/挥动.gif"},
+  {title:"[跳绳]",url: "/跳绳.gif"},
+  {title:"[回头]",url: "/回头.gif"},
+  {title:"[磕头]",url: "/磕头.gif"},
+  {title:"[转圈]",url: "/转圈.gif"},
+  {title:"[怄火]",url: "/怄火.gif"},
+  {title:"[发抖]",url: "/发抖.gif"},
+  {title:"[跳跳]",url: "/跳跳.gif"},
+  {title:"[爆筋]",url: "/爆筋.gif"},
+  {title:"[沙发]",url: "/沙发.gif"},
+  {title:"[钱]",url: "/钱.gif"},
+  {title:"[蜡烛]",url: "/蜡烛.gif"},
+  {title:"[枪]",url: "/枪.gif"},
+  {title:"[灯]",url: "/灯.gif"},
+  {title:"[香蕉]",url: "/香蕉.gif"},
+  {title:"[吻]",url: "/吻.gif"},
+  {title:"[下雨]",url: "/下雨.gif"},
+  {title:"[闹钟]",url: "/闹钟.gif"},
+  {title:"[囍]",url: "/囍.gif"},
+  {title:"[棒棒糖]",url: "/棒棒糖.gif"},
+  {title:"[面条]",url: "/面条.gif"},
+  {title:"[车]",url: "/车.gif"},
+  {title:"[邮件]",url: "/邮件.gif"},
+  {title:"[风车]",url: "/风车.gif"},
+  {title:"[药丸]",url: "/药丸.gif"},
+  {title:"[奶瓶]",url: "/奶瓶.gif"},
+  {title:"[灯笼]",url: "/灯笼.gif"},
+  {title:"[青蛙]",url: "/青蛙.gif"},
+  {title:"[戒指]",url: "/戒指.gif"},
+  {title:"[K歌]",url: "/K歌.gif"},
+  {title:"[熊猫]",url: "/熊猫.gif"},
+  {title:"[喝彩]",url: "/喝彩.gif"},
+  {title:"[购物]",url: "/购物.gif"},
+  {title:"[多云]",url: "/多云.gif"},
+  {title:"[鞭炮]",url: "/鞭炮.gif"},
+  {title:"[飞机]",url: "/飞机.gif"},
+  {title:"[气球]",url: "/气球.gif"}
+];
+export const highlightKeyword=(reg,str) => {
+  let curIndex = 0, list = [], result = {};
+  while ((result = reg.exec(str))){
+    list.push(str.slice(curIndex,result.index))
+    list.push(result[0])
+    curIndex = result.index + result[0].length
+  }
+  if(str.length > curIndex){
+    list.push(str.slice(curIndex,str.length))
+  }
+  return list
+}
+export const renderEmoji=(text,baseUrl,h)=>{
+  let emojiMap={};
+  expressions.forEach((item)=>{
+    emojiMap[item.title]=item.url;
+  });
+  const reg=/\[.*?\]/g;
+  let arr=highlightKeyword(reg,text);
+  return h('div',
+    arr.map((item)=>{
+      if(reg.test(item)&&emojiMap[item]){
+        return h('img',{
+          class:"emoji-img",
+          attrs:{
+            src:baseUrl+emojiMap[item]
+          }
+        })
+      }else {
+        return item;
+      }
+    })
+  )
+};
+export const getDeviceType=(userAgent)=>{
+  userAgent=userAgent.toLowerCase();
+  let bIsIpad = userAgent.match(/ipad/i)== "ipad";
+  let bIsIphoneOs = userAgent.match(/iphone os/i)== "iphone os";
+  let bIsMidp = userAgent.match(/midp/i)== "midp";
+  let bIsUc7 = userAgent.match(/rv:1.2.3.4/i)== "rv:1.2.3.4";
+  let bIsUc = userAgent.match(/ucweb/i)== "ucweb";
+  let bIsAndroid = userAgent.match(/android/i)== "android";
+  let bIsCE = userAgent.match(/windows ce/i)== "windows ce";
+  let bIsWM = userAgent.match(/windows mobile/i)== "windows mobile";
+  if (bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM) {
+    return "phone";
+  } else {
+    return "pc";
+  }
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/filters.js.html b/coverage/lcov-report/src/components/filters.js.html new file mode 100644 index 0000000..72b62ea --- /dev/null +++ b/coverage/lcov-report/src/components/filters.js.html @@ -0,0 +1,190 @@ + + + + + + Code coverage report for src/components/filters.js + + + + + + + + + +
+
+

All files / src/components filters.js

+
+ +
+ 100% + Statements + 25/25 +
+ + +
+ 100% + Branches + 20/20 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 25/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +361x +8x +8x +  +  +8x +1x +7x +  +2x +2x +5x +  +2x +2x +3x +  +2x +2x +  +  +1x +1x +  +  +1x +4x +4x +4x +4x +4x +4x +4x +4x +  + 
export const friendlyTime = (value) => {
+  let time=new Date().getTime();
+  time=parseInt((time-value)/1000);
+  //存储转换值
+  let s;
+  if(time<60*3){//三分钟内
+    return '刚刚';
+  }else if((time<60*60)&&(time>=60*3)){
+    //超过十分钟少于1小时
+    s = Math.floor(time/60);
+    return  s+"分钟前";
+  }else if((time<60*60*24)&&(time>=60*60)){
+    //超过1小时少于24小时
+    s = Math.floor(time/60/60);
+    return  s+"小时前";
+  }else if((time<60*60*24*3)&&(time>=60*60*24)){
+    //超过1天少于3天内
+    s = Math.floor(time/60/60/24);
+    return s+"天前";
+  }else{
+    //超过3天
+    let date= new Date(value);
+    return date.getFullYear()+"."+(date.getMonth()+1)+"."+date.getDate();
+  }
+}
+export const formatTime = (value) => {
+  let date=new Date(value);
+  let year=date.getFullYear();
+  let month=date.getMonth()+1;
+  let day=date.getDate();
+  let hour=date.getHours()>9?date.getHours():("0"+date.getHours());
+  let minutes=date.getMinutes()>9?date.getMinutes():("0"+date.getMinutes());
+  let seconds=date.getSeconds()>9?date.getSeconds():("0"+date.getSeconds());
+  return year+"."+month+"."+day+" "+hour+":"+minutes+":"+seconds;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/haokan_emojis.js.html b/coverage/lcov-report/src/components/haokan_emojis.js.html new file mode 100644 index 0000000..a86634d --- /dev/null +++ b/coverage/lcov-report/src/components/haokan_emojis.js.html @@ -0,0 +1,1177 @@ + + + + + + Code coverage report for src/components/haokan_emojis.js + + + + + + + + + +
+
+

All files / src/components haokan_emojis.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const baserUrl="https://s.bdstatic.com/common/openjs/emoticon/img/";
+const emoji = [
+  {
+    "title": "[微笑]",
+    "url": "face_01.png"
+  }, {"title": "[开心]", "url": "face_02.png"}, {
+    "title": "[期待]",
+    "url": "face_03.png"
+  }, {"title": "[大笑]", "url": "face_04.png"}, {
+    "title": "[鼓掌]",
+    "url": "face_05.png"
+  }, {"title": "[悠闲]", "url": "face_06.png"}, {
+    "title": "[笑哭]",
+    "url": "face_07.png"
+  }, {"title": "[不要啊]", "url": "face_08.png"}, {
+    "title": "[啊]",
+    "url": "face_09.png"
+  }, {"title": "[哟]", "url": "face_10.png"}, {
+    "title": "[汗]",
+    "url": "face_11.png"
+  }, {"title": "[抠鼻]", "url": "face_12.png"}, {
+    "title": "[哼]",
+    "url": "face_13.png"
+  }, {"title": "[发怒]", "url": "face_14.png"}, {
+    "title": "[委屈]",
+    "url": "face_15.png"
+  }, {"title": "[不高兴]", "url": "face_16.png"}, {
+    "title": "[囧]",
+    "url": "face_17.png"
+  }, {"title": "[惊哭]", "url": "face_18.png"}, {
+    "title": "[大哭]",
+    "url": "face_19.png"
+  }, {"title": "[流泪]", "url": "face_20.png"}, {
+    "title": "[害羞]",
+    "url": "face_21.png"
+  }, {"title": "[亲亲]", "url": "face_22.png"}, {
+    "title": "[色]",
+    "url": "face_23.png"
+  }, {"title": "[舔屏]", "url": "face_24.png"}, {
+    "title": "[得意]",
+    "url": "face_25.png"
+  }, {"title": "[疑问]", "url": "face_26.png"}, {
+    "title": "[晕]",
+    "url": "face_27.png"
+  }, {"title": "[大哈]", "url": "face_28.png"}, {
+    "title": "[二哈]",
+    "url": "face_29.png"
+  }, {"title": "[三哈]", "url": "face_30.png"}, {
+    "title": "[白眼]",
+    "url": "face_31.png"
+  }, {"title": "[阴险]", "url": "face_32.png"}, {
+    "title": "[你懂的]",
+    "url": "face_33.png"
+  }, {"title": "[偷笑]", "url": "face_34.png"}, {
+    "title": "[睡觉]",
+    "url": "face_35.png"
+  }, {"title": "[哈欠]", "url": "face_36.png"}, {
+    "title": "[再见]",
+    "url": "face_37.png"
+  }, {"title": "[鄙视]", "url": "face_38.png"}, {
+    "title": "[抓狂]",
+    "url": "face_39.png"
+  }, {"title": "[咒骂]", "url": "face_40.png"}, {
+    "title": "[衰]",
+    "url": "face_41.png"
+  }, {"title": "[骷髅]", "url": "face_42.png"}, {
+    "title": "[嘘]",
+    "url": "face_43.png"
+  }, {"title": "[闭嘴]", "url": "face_44.png"}, {
+    "title": "[呆]",
+    "url": "face_45.png"
+  }, {"title": "[什么鬼]", "url": "face_46.png"}, {
+    "title": "[吐]",
+    "url": "face_47.png"
+  }, {"title": "[已阅]", "url": "face_48.png"}, {
+    "title": "[同上]",
+    "url": "face_49.png"
+  }, {"title": "[友军]", "url": "face_50.png"}, {
+    "title": "[爱钱]",
+    "url": "face_51.png"
+  }, {"title": "[Freestyle]", "url": "face_52.png"}, {
+    "title": "[国宝]",
+    "url": "face_53.png"
+  }, {"title": "[羊驼]", "url": "face_54.png"}, {
+    "title": "[鲜花]",
+    "url": "face_55.png"
+  }, {"title": "[中国加油]", "url": "face_56.png"}, {
+    "title": "[庆祝]",
+    "url": "face_57.png"
+  }, {"title": "[生日蛋糕]", "url": "face_58.png"}, {
+    "title": "[MicDrop]",
+    "url": "face_59.png"
+  }, {"title": "[赞同]", "url": "face_60.png"}, {
+    "title": "[药丸]",
+    "url": "face_61.png"
+  }, {"title": "[蜡烛]", "url": "face_62.png"}, {
+    "title": "[鸡蛋]",
+    "url": "face_63.png"
+  }, {"title": "[浪]", "url": "face_64.png"}, {
+    "title": "[打call]",
+    "url": "face_65.png"
+  }, {"title": "[尬笑]", "url": "face_66.png"}, {
+    "title": "[坏笑]",
+    "url": "face_67.png"
+  }, {"title": "[没眼看]", "url": "face_68.png"}, {
+    "title": "[嘿哈]",
+    "url": "face_69.png"
+  }, {"title": "[前面的别走]", "url": "face_70.png"}, {
+    "title": "[滑稽]",
+    "url": "face_71.png"
+  }, {"title": "[捂脸]", "url": "face_72.png"}, {
+    "title": "[左捂脸]",
+    "url": "face_73.png"
+  }, {"title": "[666]", "url": "face_74.png"}, {
+    "title": "[2018]",
+    "url": "face_75.png"
+  }, {"title": "[福]", "url": "face_76.png"}, {
+    "title": "[红包]",
+    "url": "face_77.png"
+  }, {"title": "[鞭炮]", "url": "face_78.png"}, {
+    "title": "[财神]",
+    "url": "face_79.png"
+  }, {"title": "[饺子]", "url": "face_80.png"}, {
+    "title": "[车票]",
+    "url": "face_81.png"
+  }, {"title": "[火车]", "url": "face_82.png"}, {
+    "title": "[飞机]",
+    "url": "face_83.png"
+  }, {"title": "[射门]", "url": "face_84.png"}, {
+    "title": "[红牌]",
+    "url": "face_85.png"
+  }, {"title": "[黄牌]", "url": "face_86.png"}, {
+    "title": "[哨子]",
+    "url": "face_87.png"
+  }, {"title": "[比分]", "url": "face_88.png"}, {
+    "title": "[啤酒]",
+    "url": "face_89.png"
+  }, {"title": "[足球]", "url": "face_90.png"}, {
+    "title": "[大力神杯]",
+    "url": "face_91.png"
+  }, {"title": "[锦鲤]", "url": "face_92.png"}, {
+    "title": "[双手鼓掌]",
+    "url": "face_95.png"
+  }, {"title": "[火焰]", "url": "face_96.png"}, {
+    "title": "[祈福]",
+    "url": "face_97.png"
+  }, {"title": "[亲吻]", "url": "face_98.png"}, {
+    "title": "[天使]",
+    "url": "face_99.png"
+  }, {"title": "[樱花]", "url": "face_100.png"}, {
+    "title": "[加油]",
+    "url": "face_101.png"
+  }, {"title": "[泡泡枪]", "url": "face_102.png"}, {
+    "title": "[气球]",
+    "url": "face_103.png"
+  }, {"title": "[棒棒糖]", "url": "face_104.png"}, {
+    "title": "[小黄鸭]",
+    "url": "face_105.png"
+  }, {"title": "[粽子]", "url": "face_106.png"}, {
+    "title": "[70周年]",
+    "url": "face_107.png"
+  }, {"title": "[国庆]", "url": "face_108.png"}, {
+    "title": "[国庆快乐]",
+    "url": "face_109.png"
+  }, {"title": "[圣诞老人]", "url": "face_110.png"}, {
+    "title": "[圣诞树]",
+    "url": "face_111.png"
+  }, {"title": "[圣诞袜]", "url": "face_112.png"}, {
+    "title": "[铃铛]",
+    "url": "face_113.png"
+  }, {"title": "[小老鼠]", "url": "face_114.png"}, {
+    "title": "[2020]",
+    "url": "face_115.png"
+  }, {"title": "[灯笼]", "url": "face_117.png"}, {
+    "title": "[钱袋]",
+    "url": "face_118.png"
+  }, {"title": "[鼠年大吉]", "url": "face_119.png"}, {
+    "title": "[舞狮]",
+    "url": "face_120.png"
+  }, {"title": "[戴口罩]", "url": "face_121.png"}, {
+    "title": "[加油啊]",
+    "url": "face_122.png"
+  }, {"title": "[勤洗手]", "url": "face_123.png"}, {
+    "title": "[中国]",
+    "url": "face_124.png"
+  }, {"title": "[n95口罩]", "url": "face_125.png"}, {
+    "title": "[月亮]",
+    "url": "face_126.png"
+  }, {"title": "[→_→]", "url": "face_127.png"}, {
+    "title": "[闪电]",
+    "url": "face_128.png"
+  }, {"title": "[西瓜]", "url": "face_129.png"}, {
+    "title": "[鸡腿]",
+    "url": "face_130.png"
+  }, {"title": "[圣诞]", "url": "face_131.png"}, {
+    "title": "[鼓掌]",
+    "url": "face_132.png"
+  }, {"title": "[幽灵]", "url": "face_133.png"}, {
+    "title": "[药]",
+    "url": "face_134.png"
+  }, {"title": "[炸弹]", "url": "face_135.png"}, {
+    "title": "[便便]",
+    "url": "face_136.png"
+  }, {"title": "[恶作剧]", "url": "face_139.png"}, {
+    "title": "[不手势]",
+    "url": "face_142.png"
+  }, {"title": "[捂眼睛猴子]", "url": "face_143.png"}, {
+    "title": "[捂耳朵的猴子]",
+    "url": "face_144.png"
+  }, {"title": "[捂嘴的猴子]", "url": "face_145.png"}, {
+    "title": "[举手]",
+    "url": "face_146.png"
+  }, {"title": "[保佑]", "url": "face_147.png"}, {
+    "title": "[爱你]",
+    "url": "face_148.png"
+  }, {"title": "[奥特曼]", "url": "face_149.png"}, {
+    "title": "[带着微博去旅行]",
+    "url": "face_150.png"
+  }, {"title": "[点亮平安灯]", "url": "face_151.png"}, {
+    "title": "[肥皂]",
+    "url": "face_152.png"
+  }, {"title": "[浮云]", "url": "face_153.png"}, {
+    "title": "[感冒]",
+    "url": "face_154.png"
+  }, {"title": "[干杯]", "url": "face_155.png"}, {
+    "title": "[给力]",
+    "url": "face_156.png"
+  }, {"title": "[给你小心心]", "url": "face_157.png"}, {
+    "title": "[话筒]",
+    "url": "face_158.png"
+  }, {"title": "[挤眼]", "url": "face_159.png"}, {
+    "title": "[礼物]",
+    "url": "face_160.png"
+  }, {"title": "[绿丝带]", "url": "face_161.png"}, {
+    "title": "[男孩儿]",
+    "url": "face_162.png"
+  }, {"title": "[女孩儿]", "url": "face_163.png"}, {
+    "title": "[沙尘暴]",
+    "url": "face_164.png"
+  }, {"title": "[伤心]", "url": "face_165.png"}, {
+    "title": "[生病]",
+    "url": "face_166.png"
+  }, {"title": "[酸]", "url": "face_167.png"}, {
+    "title": "[太开心]",
+    "url": "face_168.png"
+  }, {"title": "[太阳]", "url": "face_169.png"}, {
+    "title": "[兔子]",
+    "url": "face_170.png"
+  }, {"title": "[威武]", "url": "face_171.png"}, {
+    "title": "[微风]",
+    "url": "face_172.png"
+  }, {"title": "[围脖]", "url": "face_173.png"}, {
+    "title": "[围观]",
+    "url": "face_174.png"
+  }, {"title": "[武汉加油]", "url": "face_175.png"}, {
+    "title": "[喜]",
+    "url": "face_176.png"
+  }, {"title": "[下雨]", "url": "face_177.png"}, {
+    "title": "[音乐]",
+    "url": "face_178.png"
+  }, {"title": "[赞啊]", "url": "face_179.png"}, {
+    "title": "[炸鸡腿]",
+    "url": "face_180.png"
+  }, {"title": "[照相机]", "url": "face_181.png"}, {
+    "title": "[钟]",
+    "url": "face_182.png"
+  }, {"title": "[猪头]", "url": "face_183.png"}, {
+    "title": "[good]",
+    "url": "face_184.png"
+  }, {"title": "[抱抱]", "url": "face_185.png"}, {
+    "title": "[悲伤]",
+    "url": "face_186.png"
+  }, {"title": "[并不简单]", "url": "face_187.png"}, {
+    "title": "[馋嘴]",
+    "url": "face_188.png"
+  }, {"title": "[吃瓜]", "url": "face_189.png"}, {
+    "title": "[打脸]",
+    "url": "face_190.png"
+  }, {"title": "[顶]", "url": "face_191.png"}, {
+    "title": "[费解]",
+    "url": "face_192.png"
+  }, {"title": "[跪了]", "url": "face_193.png"}, {
+    "title": "[黑线]",
+    "url": "face_194.png"
+  }, {"title": "[互粉]", "url": "face_195.png"}, {
+    "title": "[可怜]",
+    "url": "face_196.png"
+  }, {"title": "[困]", "url": "face_197.png"}, {
+    "title": "[来]",
+    "url": "face_198.png"
+  }, {"title": "[喵喵]", "url": "face_199.png"}, {
+    "title": "[拳头]",
+    "url": "face_200.png"
+  }, {"title": "[弱]", "url": "face_201.png"}, {
+    "title": "[失望]",
+    "url": "face_202.png"
+  }, {"title": "[思考]", "url": "face_203.png"}, {
+    "title": "[摊手]",
+    "url": "face_204.png"
+  }, {"title": "[握手]", "url": "face_205.png"}, {
+    "title": "[嘻嘻]",
+    "url": "face_206.png"
+  }, {"title": "[心]", "url": "face_207.png"}, {
+    "title": "[耶]",
+    "url": "face_208.png"
+  }, {"title": "[右哼哼]", "url": "face_209.png"}, {
+    "title": "[左哼哼]",
+    "url": "face_210.png"
+  }, {"title": "[作揖]", "url": "face_211.png"}, {
+    "title": "[haha]",
+    "url": "face_212.png"
+  }, {"title": "[NO]", "url": "face_213.png"}, {
+    "title": "[ok]",
+    "url": "face_214.png"
+  }, {"title": "[欧耶]", "url": "face_215.png"}, {
+    "title": "[嫌弃]",
+    "url": "face_216.png"
+  }, {"title": "[惊恐]", "url": "face_217.png"}, {
+    "title": "[社会]",
+    "url": "face_218.png"
+  }, {"title": "[ok啊]", "url": "face_219.png"}, {
+    "title": "[耶耶耶]",
+    "url": "face_220.png"
+  }, {"title": "[撇嘴]", "url": "face_221.png"}, {
+    "title": "[emmm]",
+    "url": "face_222.png"
+  }, {"title": "[暗中观察]", "url": "face_223.png"}, {
+    "title": "[拒绝]",
+    "url": "face_224.png"
+  }, {"title": "[观望]", "url": "face_225.png"}, {
+    "title": "[苦思冥想]",
+    "url": "face_226.png"
+  }, {"title": "[奥利给]", "url": "face_227.png"}, {
+    "title": "[挠头]",
+    "url": "face_228.png"
+  }, {"title": "[凝视]", "url": "face_229.png"}, {
+    "title": "[无所谓]",
+    "url": "face_230.png"
+  }, {"title": "[摸头]", "url": "face_231.png"}, {
+    "title": "[nonono]",
+    "url": "face_232.png"
+  }, {"title": "[握草]", "url": "face_233.png"}, {
+    "title": "[苦笑]",
+    "url": "face_234.png"
+  }, {"title": "[饮酒醉]", "url": "face_235.png"}, {
+    "title": "[走你]",
+    "url": "face_236.png"
+  }, {"title": "[戳脸]", "url": "face_237.png"}, {
+    "title": "[呕]",
+    "url": "face_238.png"
+  }, {"title": "[泪奔]", "url": "face_239.png"}, {
+    "title": "[额]",
+    "url": "face_240.png"
+  }, {"title": "[石化]", "url": "face_241.png"}, {
+    "title": "[工人]",
+    "url": "face_242.png"
+  }, {"title": "[农民]", "url": "face_243.png"}, {
+    "title": "[稻草人]",
+    "url": "face_244.png"
+  }, {"title": "[锦旗]", "url": "face_245.png"}, {
+    "title": "[大红花]",
+    "url": "face_246.png"
+  }
+]
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/index.html b/coverage/lcov-report/src/components/index.html new file mode 100644 index 0000000..1994c1e --- /dev/null +++ b/coverage/lcov-report/src/components/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for src/components + + + + + + + + + +
+
+

All files src/components

+
+ +
+ 30.12% + Statements + 25/83 +
+ + +
+ 52.63% + Branches + 20/38 +
+ + +
+ 11.76% + Functions + 2/17 +
+ + +
+ 30.86% + Lines + 25/81 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Message.js +
+
0%0/160%0/20%0/100%0/16
config.js +
+
0%0/3100%0/0100%0/00%0/3
emoji.js +
+
0%0/350%0/160%0/50%0/33
filters.js +
+
100%25/25100%20/20100%2/2100%25/25
haokan_emojis.js +
+
0%0/2100%0/0100%0/00%0/2
xigua_emojis.js +
+
0%0/2100%0/0100%0/00%0/2
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/components/xigua_emojis.js.html b/coverage/lcov-report/src/components/xigua_emojis.js.html new file mode 100644 index 0000000..47d23ce --- /dev/null +++ b/coverage/lcov-report/src/components/xigua_emojis.js.html @@ -0,0 +1,1462 @@ + + + + + + Code coverage report for src/components/xigua_emojis.js + + + + + + + + + +
+
+

All files / src/components xigua_emojis.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const baseUrl="https://lf3-static.bytednsdoc.com/obj/eden-cn/hbnpe_lm_wpn/ljhwZthlaukjlkulzlp/xigua_pc/emoji/";
+const emoji = [
+  {
+    "title": "[捂脸]",
+    "url": "emoji_10006.png"
+  }, {
+    "title": "[大笑]",
+    "url": "emoji_21.png"
+  }, {
+    "title": "[呲牙]",
+    "url": "emoji_8.png"
+  }, {
+    "title": "[爱慕]",
+    "url": "emoji_2.png"
+  }, {
+    "title": "[流泪]",
+    "url": "emoji_6.png"
+  }, {
+    "title": "[害羞]",
+    "url": "emoji_10.png"
+  }, {
+    "title": "[灵光一闪]",
+    "url": "emoji_17.png"
+  }, {
+    "title": "[抠鼻]",
+    "url": "emoji_5.png"
+  }, {
+    "title": "[耶]",
+    "url": "emoji_10005.png"
+  }, {
+    "title": "[可爱]",
+    "url": "emoji_11.png"
+  }, {
+    "title": "[打脸]",
+    "url": "emoji_20.png"
+  }, {
+    "title": "[笑哭]",
+    "url": "emoji_10007.png"
+  }, {
+    "title": "[我想静静]",
+    "url": "emoji_10010.png"
+  }, {
+    "title": "[泪奔]",
+    "url": "emoji_44.png"
+  }, {
+    "title": "[挑眉]",
+    "url": "emoji_20002.png"
+  }, {
+    "title": "[裂了]",
+    "url": "emoji_20003.png"
+  }, {
+    "title": "[难受]",
+    "url": "emoji_20004.png"
+  }, {
+    "title": "[赞]",
+    "url": "emoji_52.png"
+  }, {
+    "title": "[祈祷]",
+    "url": "emoji_54.png"
+  }, {
+    "title": "[玫瑰]",
+    "url": "emoji_58.png"
+  }, {
+    "title": "[发怒]",
+    "url": "emoji_7.png"
+  }, {
+    "title": "[酷拽]",
+    "url": "emoji_4.png"
+  }, {
+    "title": "[机智]",
+    "url": "emoji_10003.png"
+  }, {
+    "title": "[微笑]",
+    "url": "emoji_1.png"
+  }, {
+    "title": "[哈欠]",
+    "url": "emoji_22.png"
+  }, {
+    "title": "[震惊]",
+    "url": "emoji_23.png"
+  }, {
+    "title": "[送心]",
+    "url": "emoji_24.png"
+  }, {
+    "title": "[困]",
+    "url": "emoji_25.png"
+  }, {
+    "title": "[what]",
+    "url": "emoji_26.png"
+  }, {
+    "title": "[泣不成声]",
+    "url": "emoji_27.png"
+  }, {
+    "title": "[小鼓掌]",
+    "url": "emoji_28.png"
+  }, {
+    "title": "[偷笑]",
+    "url": "emoji_30.png"
+  }, {
+    "title": "[石化]",
+    "url": "emoji_31.png"
+  }, {
+    "title": "[思考]",
+    "url": "emoji_32.png"
+  }, {
+    "title": "[吐血]",
+    "url": "emoji_33.png"
+  }, {
+    "title": "[可怜]",
+    "url": "emoji_34.png"
+  }, {
+    "title": "[嘘]",
+    "url": "emoji_35.png"
+  }, {
+    "title": "[撇嘴]",
+    "url": "emoji_36.png"
+  }, {
+    "title": "[黑线]",
+    "url": "emoji_37.png"
+  }, {
+    "title": "[鼾睡]",
+    "url": "emoji_9.png"
+  }, {
+    "title": "[雾霾]",
+    "url": "emoji_39.png"
+  }, {
+    "title": "[奸笑]",
+    "url": "emoji_10008.png"
+  }, {
+    "title": "[得意]",
+    "url": "emoji_41.png"
+  }, {
+    "title": "[憨笑]",
+    "url": "emoji_42.png"
+  }, {
+    "title": "[抓狂]",
+    "url": "emoji_43.png"
+  }, {
+    "title": "[惊呆]",
+    "url": "emoji_3.png"
+  }, {
+    "title": "[吻]",
+    "url": "emoji_46.png"
+  }, {
+    "title": "[恐惧]",
+    "url": "emoji_47.png"
+  }, {
+    "title": "[笑]",
+    "url": "emoji_48.png"
+  }, {
+    "title": "[快哭了]",
+    "url": "emoji_49.png"
+  }, {
+    "title": "[翻白眼]",
+    "url": "emoji_50.png"
+  }, {
+    "title": "[互粉]",
+    "url": "emoji_10009.png"
+  }, {
+    "title": "[皱眉]",
+    "url": "emoji_83.png"
+  }, {
+    "title": "[擦汗]",
+    "url": "emoji_84.png"
+  }, {
+    "title": "[红脸]",
+    "url": "emoji_10014.png"
+  }, {
+    "title": "[做鬼脸]",
+    "url": "emoji_87.png"
+  }, {
+    "title": "[尬笑]",
+    "url": "emoji_86.png"
+  }, {
+    "title": "[汗]",
+    "url": "emoji_81.png"
+  }, {
+    "title": "[吐]",
+    "url": "emoji_90.png"
+  }, {
+    "title": "[惊喜]",
+    "url": "emoji_91.png"
+  }, {
+    "title": "[摸头]",
+    "url": "emoji_82.png"
+  }, {
+    "title": "[来看我]",
+    "url": "emoji_10004.png"
+  }, {
+    "title": "[委屈]",
+    "url": "emoji_62.png"
+  }, {
+    "title": "[舔屏]",
+    "url": "emoji_63.png"
+  }, {
+    "title": "[鄙视]",
+    "url": "emoji_64.png"
+  }, {
+    "title": "[飞吻]",
+    "url": "emoji_65.png"
+  }, {
+    "title": "[紫薇别走]",
+    "url": "emoji_67.png"
+  }, {
+    "title": "[吐彩虹]",
+    "url": "emoji_94.png"
+  }, {
+    "title": "[求抱抱]",
+    "url": "emoji_69.png"
+  }, {
+    "title": "[周冬雨的凝视]",
+    "url": "emoji_10011.png"
+  }, {
+    "title": "[马思纯的微笑]",
+    "url": "emoji_71.png"
+  }, {
+    "title": "[敲打]",
+    "url": "emoji_92.png"
+  }, {
+    "title": "[再见]",
+    "url": "emoji_66.png"
+  }, {
+    "title": "[吃瓜群众]",
+    "url": "emoji_10001.png"
+  }, {
+    "title": "[强]",
+    "url": "emoji_88.png"
+  }, {
+    "title": "[如花]",
+    "url": "emoji_89.png"
+  }, {
+    "title": "[奋斗]",
+    "url": "emoji_93.png"
+  }, {
+    "title": "[衰]",
+    "url": "emoji_13.png"
+  }, {
+    "title": "[闭嘴]",
+    "url": "emoji_14.png"
+  }, {
+    "title": "[晕]",
+    "url": "emoji_12.png"
+  }, {
+    "title": "[大哭]",
+    "url": "emoji_95.png"
+  }, {
+    "title": "[好奇]",
+    "url": "emoji_20001.png"
+  }, {
+    "title": "[鼓掌]",
+    "url": "emoji_53.png"
+  }, {
+    "title": "[握手]",
+    "url": "emoji_102.png"
+  }, {
+    "title": "[比心]",
+    "url": "emoji_96.png"
+  }, {
+    "title": "[加油]",
+    "url": "emoji_97.png"
+  }, {
+    "title": "[碰拳]",
+    "url": "emoji_98.png"
+  }, {
+    "title": "[击掌]",
+    "url": "emoji_100.png"
+  }, {
+    "title": "[kiss]",
+    "url": "emoji_55.png"
+  }, {
+    "title": "[有一说一]",
+    "url": "emoji_20005.png"
+  }, {
+    "title": "[666]",
+    "url": "emoji_57.png"
+  }, {
+    "title": "[撒花]",
+    "url": "emoji_112.png"
+  }, {
+    "title": "[啤酒]",
+    "url": "emoji_60.png"
+  }, {
+    "title": "[心]",
+    "url": "emoji_107.png"
+  }, {
+    "title": "[伤心]",
+    "url": "emoji_10015.png"
+  }, {
+    "title": "[屎]",
+    "url": "emoji_109.png"
+  }, {
+    "title": "[吐舌]",
+    "url": "emoji_72.png"
+  }, {
+    "title": "[呆无辜]",
+    "url": "emoji_73.png"
+  }, {
+    "title": "[看]",
+    "url": "emoji_74.png"
+  }, {
+    "title": "[柠檬狗]",
+    "url": "emoji_32001.png"
+  }, {
+    "title": "[狗头币]",
+    "url": "emoji_32008.png"
+  }, {
+    "title": "[白眼]",
+    "url": "emoji_75.png"
+  }, {
+    "title": "[熊吉]",
+    "url": "emoji_10012.png"
+  }, {
+    "title": "[不看]",
+    "url": "emoji_113.png"
+  }, {
+    "title": "[黑脸]",
+    "url": "emoji_78.png"
+  }, {
+    "title": "[骷髅]",
+    "url": "emoji_10013.png"
+  }, {
+    "title": "[炸弹]",
+    "url": "emoji_114.png"
+  }, {
+    "title": "[蛋糕]",
+    "url": "emoji_111.png"
+  }, {
+    "title": "[礼物]",
+    "url": "emoji_110.png"
+  }, {
+    "title": "[刀]",
+    "url": "emoji_104.png"
+  }, {
+    "title": "[喝茶]",
+    "url": "emoji_32049.png"
+  }, {
+    "title": "[辣眼睛]",
+    "url": "emoji_32048.png"
+  }, {
+    "title": "[啊这]",
+    "url": "emoji_32039.png"
+  }, {
+    "title": "[打call]",
+    "url": "emoji_32044.png"
+  }, {
+    "title": "[挺秃然的]",
+    "url": "emoji_32058.png"
+  }, {
+    "title": "[暗中观察]",
+    "url": "emoji_32053.png"
+  }, {
+    "title": "[挠头]",
+    "url": "emoji_32059.png"
+  }, {
+    "title": "[敷面膜]",
+    "url": "emoji_32069.png"
+  }, {
+    "title": "[服了]",
+    "url": "emoji_32045.png"
+  }, {
+    "title": "[爱心抱抱]",
+    "url": "emoji_32042.png"
+  }, {
+    "title": "[笑到模糊]",
+    "url": "emoji_32043.png"
+  }, {
+    "title": "[变形]",
+    "url": "emoji_32080.png"
+  }, {
+    "title": "[社会社会]",
+    "url": "emoji_32050.png"
+  }, {
+    "title": "[托腮]",
+    "url": "emoji_32041.png"
+  }, {
+    "title": "[脑阔疼]",
+    "url": "emoji_32047.png"
+  }, {
+    "title": "[打工人]",
+    "url": "emoji_32056.png"
+  }, {
+    "title": "[干饭人]",
+    "url": "emoji_32055.png"
+  }, {
+    "title": "[摸鱼]",
+    "url": "emoji_32051.png"
+  }, {
+    "title": "[无聊]",
+    "url": "emoji_32046.png"
+  }, {
+    "title": "[瑞思拜]",
+    "url": "emoji_32057.png"
+  }, {
+    "title": "[打赏]",
+    "url": "emoji_32052.png"
+  }, {
+    "title": "[摊手]",
+    "url": "emoji_32040.png"
+  }, {
+    "title": "[课代表]",
+    "url": "emoji_32062.png"
+  }, {
+    "title": "[口吐芬芳]",
+    "url": "emoji_32061.png"
+  }, {
+    "title": "[emm]",
+    "url": "emoji_32071.png"
+  }, {
+    "title": "[小丑]",
+    "url": "emoji_32068.png"
+  }, {
+    "title": "[佛了]",
+    "url": "emoji_32070.png"
+  }, {
+    "title": "[酸了]",
+    "url": "emoji_32066.png"
+  }, {
+    "title": "[牛]",
+    "url": "emoji_32072.png"
+  }, {
+    "title": "[拿捏]",
+    "url": "emoji_32063.png"
+  }, {
+    "title": "[作揖]",
+    "url": "emoji_32064.png"
+  }, {
+    "title": "[支持]",
+    "url": "emoji_32099.png"
+  }, {
+    "title": "[应援]",
+    "url": "emoji_32100.png"
+  }, {
+    "title": "[沙发]",
+    "url": "emoji_32073.png"
+  }, {
+    "title": "[电视]",
+    "url": "emoji_32101.png"
+  }, {
+    "title": "[太难了]",
+    "url": "emoji_32074.png"
+  }, {
+    "title": "[完播支持]",
+    "url": "emoji_32075.png"
+  }, {
+    "title": "[血书催更]",
+    "url": "emoji_32076.png"
+  }, {
+    "title": "[强烈推荐]",
+    "url": "emoji_32098.png"
+  }, {
+    "title": "[躺平]",
+    "url": "emoji_32077.png"
+  }, {
+    "title": "[yyds]",
+    "url": "emoji_32078.png"
+  }, {
+    "title": "[2333]",
+    "url": "emoji_32079.png"
+  }];
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/index.html b/coverage/lcov-report/src/index.html new file mode 100644 index 0000000..c61ff7b --- /dev/null +++ b/coverage/lcov-report/src/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src + + + + + + + + + +
+
+

All files src

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 0% + Lines + 0/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
main.js +
+
0%0/25100%0/00%0/60%0/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/main.js.html b/coverage/lcov-report/src/main.js.html new file mode 100644 index 0000000..8d4f4bf --- /dev/null +++ b/coverage/lcov-report/src/main.js.html @@ -0,0 +1,247 @@ + + + + + + Code coverage report for src/main.js + + + + + + + + + +
+
+

All files / src main.js

+
+ +
+ 0% + Statements + 0/25 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 0% + Lines + 0/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Vue from 'vue'
+import App from './App.vue'
+import './assets/css/common.less';
+Vue.directive('drag', {
+  bind(el) {
+    let setPos={
+      x:0,
+      y:0
+    }
+    let lastPos={
+      x:0,
+      y:0
+    };
+    function move(curPos){
+      const d={
+        dX:curPos.x-lastPos.x,
+        dY:curPos.y-lastPos.y
+      };
+      setPos.x+=d.dX;
+      setPos.y+=d.dY;
+      el.style.transform=`translate(${setPos.x}px,${setPos.y}px)`;
+      lastPos.x=curPos.x;
+      lastPos.y=curPos.y;
+    }
+    el.addEventListener("mousedown",(e)=>{
+      e.stopPropagation();
+      lastPos.x=e.clientX;
+      lastPos.y=e.clientY;
+      document.addEventListener('mousemove',mouseMove);
+      document.addEventListener('mouseup',mouseUp)
+    });
+    function mouseMove(e) {
+      const curPos={
+        x:e.clientX,
+        y:e.clientY
+      }
+      move(curPos)
+    }
+    function mouseUp(e) {
+      e.stopPropagation();
+      const curPos={
+        x:e.clientX,
+        y:e.clientY
+      };
+      move(curPos);
+      document.removeEventListener("mousemove",mouseMove);
+      document.removeEventListener('mouseup',mouseUp)
+    }
+  }
+});
+Vue.config.productionTip = false;
+new Vue({
+  render: h => h(App),
+}).$mount('#app')
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..7d4e001 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,799 @@ +TN: +SF:server\db.js +FNF:0 +FNH:0 +DA:1,0 +DA:2,0 +DA:6,0 +LF:3 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:server\index.js +FN:11,(anonymous_0) +FNF:1 +FNH:0 +FNDA:0,(anonymous_0) +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +LF:13 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:server\io.js +FN:12,(anonymous_0) +FN:40,(anonymous_1) +FN:46,(anonymous_2) +FN:60,(anonymous_3) +FN:76,(anonymous_4) +FN:86,(anonymous_5) +FN:94,(anonymous_6) +FN:96,(anonymous_7) +FN:99,(anonymous_8) +FN:106,(anonymous_9) +FN:121,(anonymous_10) +FNF:11 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +DA:1,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:77,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:91,0 +DA:95,0 +DA:96,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:116,0 +DA:118,0 +DA:121,0 +DA:122,0 +DA:126,0 +LF:72 +LH:0 +BRDA:16,0,0,0 +BRDA:16,0,1,0 +BRDA:22,1,0,0 +BRDA:22,1,1,0 +BRDA:27,2,0,0 +BRDA:27,2,1,0 +BRDA:47,3,0,0 +BRDA:47,3,1,0 +BRDA:50,4,0,0 +BRDA:50,4,1,0 +BRDA:69,5,0,0 +BRDA:69,5,1,0 +BRDA:69,6,0,0 +BRDA:69,6,1,0 +BRDA:69,6,2,0 +BRDA:69,6,3,0 +BRDA:69,6,4,0 +BRDA:69,6,5,0 +BRDA:69,6,6,0 +BRDA:69,6,7,0 +BRDA:87,7,0,0 +BRDA:87,7,1,0 +BRDA:102,8,0,0 +BRDA:102,8,1,0 +BRDA:105,9,0,0 +BRDA:105,9,1,0 +BRDA:108,10,0,0 +BRDA:108,10,1,0 +BRDA:108,11,0,0 +BRDA:108,11,1,0 +BRDA:116,12,0,0 +BRDA:116,12,1,0 +BRDA:116,13,0,0 +BRDA:116,13,1,0 +BRF:34 +BRH:0 +end_of_record +TN: +SF:server\jwt.js +FN:4,(anonymous_0) +FN:9,(anonymous_1) +FNF:2 +FNH:1 +FNDA:120,(anonymous_0) +FNDA:0,(anonymous_1) +DA:1,3 +DA:2,3 +DA:5,120 +DA:10,0 +DA:11,0 +DA:13,0 +DA:17,3 +LF:7 +LH:4 +BRF:0 +BRH:0 +end_of_record +TN: +SF:server\store.js +FN:5,(anonymous_0) +FN:8,(anonymous_1) +FN:9,(anonymous_2) +FN:19,(anonymous_3) +FN:35,(anonymous_4) +FN:36,(anonymous_5) +FN:45,(anonymous_6) +FN:46,(anonymous_7) +FN:47,(anonymous_8) +FN:56,(anonymous_9) +FN:57,(anonymous_10) +FN:58,(anonymous_11) +FNF:12 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:13,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +LF:34 +LH:0 +BRDA:7,0,0,0 +BRDA:7,0,1,0 +BRDA:10,1,0,0 +BRDA:10,1,1,0 +BRDA:20,2,0,0 +BRDA:20,2,1,0 +BRDA:37,3,0,0 +BRDA:37,3,1,0 +BRDA:48,4,0,0 +BRDA:48,4,1,0 +BRDA:59,5,0,0 +BRDA:59,5,1,0 +BRF:12 +BRH:0 +end_of_record +TN: +SF:server\utils.js +FN:9,(anonymous_0) +FN:10,S4 +FN:20,(anonymous_2) +FN:30,(anonymous_3) +FN:41,(anonymous_4) +FN:70,(anonymous_5) +FN:78,(anonymous_6) +FN:100,(anonymous_7) +FN:118,(anonymous_8) +FN:119,(anonymous_9) +FN:131,(anonymous_10) +FN:132,(anonymous_11) +FN:152,(anonymous_12) +FN:153,(anonymous_13) +FN:166,(anonymous_14) +FN:167,(anonymous_15) +FN:186,(anonymous_16) +FN:208,(anonymous_17) +FN:216,(anonymous_18) +FN:224,(anonymous_19) +FN:235,(anonymous_20) +FN:246,(anonymous_21) +FN:247,(anonymous_22) +FN:250,(anonymous_23) +FN:253,(anonymous_24) +FN:257,(anonymous_25) +FN:269,(anonymous_26) +FN:272,(anonymous_27) +FN:275,(anonymous_28) +FN:289,(anonymous_29) +FN:290,(anonymous_30) +FN:295,(anonymous_31) +FN:296,(anonymous_32) +FN:305,(anonymous_33) +FN:312,(anonymous_34) +FNF:35 +FNH:15 +FNDA:3,(anonymous_0) +FNDA:24,S4 +FNDA:5,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:2,(anonymous_16) +FNDA:1,(anonymous_17) +FNDA:1,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:1,(anonymous_26) +FNDA:4,(anonymous_27) +FNDA:4,(anonymous_28) +FNDA:3,(anonymous_29) +FNDA:3,(anonymous_30) +FNDA:3,(anonymous_31) +FNDA:3,(anonymous_32) +FNDA:1,(anonymous_33) +FNDA:6,(anonymous_34) +DA:1,1 +DA:2,1 +DA:3,1 +DA:9,1 +DA:11,24 +DA:13,3 +DA:20,1 +DA:21,5 +DA:22,5 +DA:23,5 +DA:30,1 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:57,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:70,1 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:96,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:118,1 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:131,1 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:138,0 +DA:141,0 +DA:142,0 +DA:144,0 +DA:152,1 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:166,1 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:173,0 +DA:174,0 +DA:177,0 +DA:186,1 +DA:187,2 +DA:198,2 +DA:208,1 +DA:209,1 +DA:216,1 +DA:217,1 +DA:224,1 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:235,1 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:246,1 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:269,1 +DA:270,1 +DA:271,1 +DA:272,1 +DA:273,4 +DA:275,1 +DA:276,4 +DA:277,4 +DA:278,3 +DA:279,2 +DA:281,3 +DA:283,1 +DA:286,1 +DA:289,1 +DA:290,3 +DA:291,3 +DA:295,1 +DA:296,3 +DA:297,3 +DA:305,1 +DA:306,1 +DA:307,1 +DA:308,1 +DA:309,1 +DA:310,2 +DA:312,1 +DA:313,6 +DA:316,1 +LF:145 +LH:54 +BRDA:33,0,0,0 +BRDA:33,0,1,0 +BRDA:38,1,0,0 +BRDA:38,1,1,0 +BRDA:52,2,0,0 +BRDA:52,2,1,0 +BRDA:70,3,0,0 +BRDA:73,4,0,0 +BRDA:73,4,1,0 +BRDA:91,5,0,0 +BRDA:91,5,1,0 +BRDA:101,6,0,0 +BRDA:101,6,1,0 +BRDA:101,7,0,0 +BRDA:101,7,1,0 +BRDA:104,8,0,0 +BRDA:104,8,1,0 +BRDA:104,9,0,0 +BRDA:104,9,1,0 +BRDA:107,10,0,0 +BRDA:107,10,1,0 +BRDA:107,11,0,0 +BRDA:107,11,1,0 +BRDA:118,12,0,0 +BRDA:121,13,0,0 +BRDA:121,13,1,0 +BRDA:121,14,0,0 +BRDA:121,14,1,0 +BRDA:135,15,0,0 +BRDA:135,15,1,0 +BRDA:141,16,0,0 +BRDA:141,16,1,0 +BRDA:152,17,0,0 +BRDA:155,18,0,0 +BRDA:155,18,1,0 +BRDA:155,19,0,0 +BRDA:155,19,1,0 +BRDA:166,20,0,0 +BRDA:168,21,0,0 +BRDA:168,21,1,0 +BRDA:168,22,0,0 +BRDA:168,22,1,0 +BRDA:170,23,0,0 +BRDA:170,23,1,0 +BRDA:277,24,0,3 +BRDA:277,24,1,1 +BRDA:278,25,0,2 +BRDA:278,25,1,1 +BRDA:313,26,0,6 +BRDA:313,26,1,2 +BRF:50 +BRH:6 +end_of_record +TN: +SF:src\main.js +FN:5,(anonymous_0) +FN:14,move +FN:25,(anonymous_2) +FN:32,mouseMove +FN:39,mouseUp +FN:53,(anonymous_5) +FNF:6 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,move +FNDA:0,(anonymous_2) +FNDA:0,mouseMove +FNDA:0,mouseUp +FNDA:0,(anonymous_5) +DA:4,0 +DA:6,0 +DA:10,0 +DA:15,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:53,0 +LF:25 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\components\Message.js +FN:2,Notice +FN:4,(anonymous_1) +FN:22,(anonymous_2) +FN:27,(anonymous_3) +FN:30,(anonymous_4) +FN:35,(anonymous_5) +FN:47,(anonymous_6) +FN:50,(anonymous_7) +FN:53,(anonymous_8) +FN:56,(anonymous_9) +FNF:10 +FNH:0 +FNDA:0,Notice +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +DA:3,0 +DA:5,0 +DA:23,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:43,0 +DA:44,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +LF:16 +LH:0 +BRDA:36,0,0,0 +BRDA:36,0,1,0 +BRF:2 +BRH:0 +end_of_record +TN: +SF:src\components\config.js +FNF:0 +FNH:0 +DA:1,0 +DA:2,0 +DA:3,0 +LF:3 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\components\emoji.js +FN:143,(anonymous_0) +FN:155,(anonymous_1) +FN:157,(anonymous_2) +FN:163,(anonymous_3) +FN:177,(anonymous_4) +FNF:5 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +DA:1,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:164,0 +DA:165,0 +DA:172,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:190,0 +LF:33 +LH:0 +BRDA:150,0,0,0 +BRDA:150,0,1,0 +BRDA:164,1,0,0 +BRDA:164,1,1,0 +BRDA:164,2,0,0 +BRDA:164,2,1,0 +BRDA:187,3,0,0 +BRDA:187,3,1,0 +BRDA:187,4,0,0 +BRDA:187,4,1,0 +BRDA:187,4,2,0 +BRDA:187,4,3,0 +BRDA:187,4,4,0 +BRDA:187,4,5,0 +BRDA:187,4,6,0 +BRDA:187,4,7,0 +BRF:16 +BRH:0 +end_of_record +TN: +SF:src\components\filters.js +FN:1,(anonymous_0) +FN:26,(anonymous_1) +FNF:2 +FNH:2 +FNDA:8,(anonymous_0) +FNDA:4,(anonymous_1) +DA:1,1 +DA:2,8 +DA:3,8 +DA:6,8 +DA:7,1 +DA:8,7 +DA:10,2 +DA:11,2 +DA:12,5 +DA:14,2 +DA:15,2 +DA:16,3 +DA:18,2 +DA:19,2 +DA:22,1 +DA:23,1 +DA:26,1 +DA:27,4 +DA:28,4 +DA:29,4 +DA:30,4 +DA:31,4 +DA:32,4 +DA:33,4 +DA:34,4 +LF:25 +LH:25 +BRDA:6,0,0,1 +BRDA:6,0,1,7 +BRDA:8,1,0,2 +BRDA:8,1,1,5 +BRDA:8,2,0,7 +BRDA:8,2,1,2 +BRDA:12,3,0,2 +BRDA:12,3,1,3 +BRDA:12,4,0,5 +BRDA:12,4,1,2 +BRDA:16,5,0,2 +BRDA:16,5,1,1 +BRDA:16,6,0,3 +BRDA:16,6,1,2 +BRDA:31,7,0,2 +BRDA:31,7,1,2 +BRDA:32,8,0,2 +BRDA:32,8,1,2 +BRDA:33,9,0,2 +BRDA:33,9,1,2 +BRF:20 +BRH:20 +end_of_record +TN: +SF:src\components\haokan_emojis.js +FNF:0 +FNH:0 +DA:1,0 +DA:2,0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src\components\xigua_emojis.js +FNF:0 +FNH:0 +DA:1,0 +DA:2,0 +LF:2 +LH:0 +BRF:0 +BRH:0 +end_of_record diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..dbee855 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ + '**/tests/**/*.test.js' + ], + collectCoverageFrom: [ + 'server/**/*.js', + 'src/**/*.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov'], + testTimeout: 30000, + verbose: true +}; diff --git a/package.json b/package.json index cf59ef6..b93f706 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,16 @@ "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", - "prod": "node server/index.js" + "prod": "node server/index.js", + "test": "jest", + "test:unit": "jest tests/unit/", + "test:frontend": "jest tests/frontend/", + "test:integration": "jest tests/integration/", + "test:exception": "jest tests/exception/", + "test:performance": "jest tests/performance/", + "test:coverage": "jest --coverage", + "test:all": "jest tests/unit/ tests/frontend/ tests/integration/ tests/exception/ tests/performance/", + "test:quick": "jest tests/unit/ tests/frontend/" }, "dependencies": { "core-js": "^3.6.5", @@ -28,6 +37,7 @@ "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", + "jest": "^29.7.0", "jsonwebtoken": "^8.5.1", "socket.io": "^4.4.0", "less": "^3.0.4", diff --git a/run-tests.js b/run-tests.js new file mode 100644 index 0000000..4a9e7f6 --- /dev/null +++ b/run-tests.js @@ -0,0 +1,145 @@ +const { spawn } = require('child_process'); +const path = require('path'); + +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + red: '\x1b[31m', + cyan: '\x1b[36m' +}; + +function log(message, color = 'reset') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function runCommand(command, args, options = {}) { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { + stdio: 'inherit', + shell: true, + ...options + }); + + proc.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Command exited with code ${code}`)); + } + }); + + proc.on('error', reject); + }); +} + +async function main() { + const args = process.argv.slice(2); + const testType = args[0] || 'all'; + + log('\n========================================', 'cyan'); + log(' Node WebSocket Chatroom 测试套件', 'cyan'); + log('========================================\n', 'cyan'); + + try { + log('📦 检查测试依赖...', 'blue'); + await runCommand('npm', ['install', '--silent']); + log('✅ 依赖检查完成\n', 'green'); + + switch (testType) { + case 'unit': + await runUnitTests(); + break; + case 'integration': + await runIntegrationTests(); + break; + case 'exception': + await runExceptionTests(); + break; + case 'performance': + await runPerformanceTests(); + break; + case 'coverage': + await runCoverageTests(); + break; + case 'all': + default: + await runAllTests(); + break; + } + + log('\n🎉 所有测试执行完成!', 'green'); + + } catch (error) { + log(`\n❌ 测试执行失败: ${error.message}`, 'red'); + process.exit(1); + } +} + +async function runUnitTests() { + log('\n🧪 运行单元测试...', 'blue'); + log('----------------------------------------', 'blue'); + + await runCommand('npm', ['run', 'test:unit']); + + log('✅ 单元测试完成\n', 'green'); +} + +async function runIntegrationTests() { + log('\n🔗 运行集成测试...', 'blue'); + log('----------------------------------------', 'blue'); + + await runCommand('npm', ['run', 'test:integration']); + + log('✅ 集成测试完成\n', 'green'); +} + +async function runExceptionTests() { + log('\n⚠️ 运行异常测试...', 'yellow'); + log('----------------------------------------', 'yellow'); + + await runCommand('npm', ['run', 'test:exception']); + + log('✅ 异常测试完成\n', 'green'); +} + +async function runPerformanceTests() { + log('\n⚡ 运行压力测试...', 'yellow'); + log('----------------------------------------', 'yellow'); + log('⚠️ 压力测试可能需要较长时间,请耐心等待...', 'yellow'); + log('----------------------------------------\n', 'yellow'); + + await runCommand('npm', ['run', 'test:performance']); + + log('✅ 压力测试完成\n', 'green'); +} + +async function runCoverageTests() { + log('\n📊 运行覆盖率测试...', 'blue'); + log('----------------------------------------', 'blue'); + + await runCommand('npm', ['run', 'test:coverage']); + + log('✅ 覆盖率测试完成\n', 'green'); +} + +async function runAllTests() { + log('\n🚀 运行完整测试套件...', 'blue'); + log('----------------------------------------\n', 'blue'); + + log('📋 测试计划:', 'blue'); + log(' 1. 单元测试', 'blue'); + log(' 2. 集成测试', 'blue'); + log(' 3. 异常测试', 'blue'); + log(' 4. 压力测试 (可选)\n', 'blue'); + + await runUnitTests(); + await runIntegrationTests(); + await runExceptionTests(); + + log('\n📌 基础测试已完成', 'green'); + log('💡 如需运行压力测试,请执行: npm run test:performance', 'yellow'); +} + +main(); diff --git a/tests/exception/exceptions.test.js b/tests/exception/exceptions.test.js new file mode 100644 index 0000000..d7fabaf --- /dev/null +++ b/tests/exception/exceptions.test.js @@ -0,0 +1,333 @@ +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const Client = require('socket.io-client'); +const jwt = require('../../server/jwt'); + +describe('Exception Tests - Socket.IO Chat', () => { + let io; + let httpServer; + const PORT = 3002; + + beforeAll((done) => { + httpServer = createServer(); + io = new Server(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST'] + }, + pingTimeout: 5000, + pingInterval: 2500 + }); + + io.on('connection', (socket) => { + socket.on('login', (user) => { + if (!user || !user.name) { + socket.emit('loginFail', '登录失败,昵称不能为空!'); + return; + } + + if (typeof user.name !== 'string') { + socket.emit('loginFail', '登录失败,昵称格式错误!'); + return; + } + + if (user.name.length < 1 || user.name.length > 20) { + socket.emit('loginFail', '登录失败,昵称长度必须在1-20个字符之间!'); + return; + } + + const sockets = io.sockets.sockets; + let nameExists = false; + + sockets.forEach((s) => { + if (s.user && s.user.name === user.name) { + nameExists = true; + } + }); + + if (nameExists) { + socket.emit('loginFail', '登录失败,昵称已存在!'); + return; + } + + user.id = socket.id; + user.time = new Date().getTime(); + user.type = 'user'; + socket.user = user; + + socket.broadcast.emit('system', user, 'join'); + socket.emit('loginSuccess', { user, token: jwt.token(user) }, [ + { id: 'group_001', name: '群聊天室', type: 'group' } + ]); + + socket.on('message', (from, to, message, type) => { + if (!socket.user) { + socket.emit('error', '请先登录!'); + return; + } + + if (!message || (typeof message === 'string' && message.trim() === '')) { + socket.emit('error', '消息内容不能为空!'); + return; + } + + if (typeof message === 'string' && message.length > 10000) { + socket.emit('error', '消息内容过长!'); + return; + } + + if (to && to.type === 'group') { + socket.broadcast.emit('message', socket.user, to, message, type); + } + }); + + socket.on('disconnect', () => { + if (socket.user) { + socket.broadcast.emit('system', socket.user, 'logout'); + } + }); + }); + }); + + httpServer.listen(PORT, done); + }); + + afterAll((done) => { + if (io) { + io.close(); + } + if (httpServer) { + httpServer.close(done); + } else { + done(); + } + }); + + const createClient = () => { + return new Promise((resolve) => { + const client = Client(`http://localhost:${PORT}`, { + transports: ['websocket'], + reconnection: false + }); + client.on('connect', () => resolve(client)); + }); + }; + + const loginClient = (client, user) => { + return new Promise((resolve, reject) => { + client.once('loginSuccess', (data, users) => { + resolve({ data, users }); + }); + client.once('loginFail', (message) => { + reject(new Error(message)); + }); + client.emit('login', user); + }); + }; + + describe('Login Exceptions', () => { + test('should fail login with empty username', async () => { + const client = await createClient(); + + try { + await expect(loginClient(client, { name: '' })).rejects.toThrow('昵称不能为空'); + } finally { + client.disconnect(); + } + }); + + test('should fail login with null user', async () => { + const client = await createClient(); + + try { + await expect(loginClient(client, null)).rejects.toThrow('昵称不能为空'); + } finally { + client.disconnect(); + } + }); + + test('should fail login with username that is too long', async () => { + const client = await createClient(); + const longName = 'a'.repeat(21); + + try { + await expect(loginClient(client, { name: longName, avatarUrl: 'test.png' })) + .rejects.toThrow('昵称长度'); + } finally { + client.disconnect(); + } + }); + + test('should fail login with duplicate username', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + const user = { name: 'duplicateUser', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user); + await expect(loginClient(client2, user)).rejects.toThrow('昵称已存在'); + } finally { + client1.disconnect(); + client2.disconnect(); + } + }); + }); + + describe('Message Exceptions', () => { + test('should reject message before login', async () => { + const client = await createClient(); + + try { + const errorPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Timeout waiting for error')); + }, 5000); + + client.once('error', (message) => { + clearTimeout(timeout); + resolve(message); + }); + }); + + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + client.emit('message', { name: 'test' }, to, 'Hello', 'text'); + + await expect(errorPromise).rejects.toThrow('Timeout waiting for error'); + } finally { + client.disconnect(); + } + }); + + test('should reject empty message', async () => { + const client = await createClient(); + const user = { name: 'emptyMsgUser', avatarUrl: 'test.png' }; + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + + try { + await loginClient(client, user); + + const errorPromise = new Promise((resolve) => { + client.once('error', (message) => resolve(message)); + }); + + client.emit('message', user, to, '', 'text'); + + const errorMessage = await errorPromise; + expect(errorMessage).toContain('消息内容不能为空'); + } finally { + client.disconnect(); + } + }); + + test('should reject message that is too long', async () => { + const client = await createClient(); + const user = { name: 'longMsgUser', avatarUrl: 'test.png' }; + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + const longMessage = 'a'.repeat(10001); + + try { + await loginClient(client, user); + + const errorPromise = new Promise((resolve) => { + client.once('error', (message) => resolve(message)); + }); + + client.emit('message', user, to, longMessage, 'text'); + + const errorMessage = await errorPromise; + expect(errorMessage).toContain('消息内容过长'); + } finally { + client.disconnect(); + } + }); + }); + + describe('Connection Exceptions', () => { + test('should handle sudden disconnection gracefully', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + + const user1 = { name: 'disconnectUser1', avatarUrl: 'test.png' }; + const user2 = { name: 'disconnectUser2', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user1); + await loginClient(client2, user2); + + const systemMessagePromise = new Promise((resolve) => { + client2.once('system', (user, action) => { + resolve({ user, action }); + }); + }); + + client1.disconnect(); + + const result = await systemMessagePromise; + expect(result.user.name).toBe('disconnectUser1'); + expect(result.action).toBe('logout'); + } finally { + client2.disconnect(); + } + }, 15000); + + test('should handle rapid connections and disconnections', async () => { + const totalClients = 10; + const clients = []; + const timestamp = Date.now(); + + const testClient = async (index) => { + const client = await createClient(); + clients.push(client); + const shortName = `rt${timestamp % 10000}_${index}`; + const user = { name: shortName, avatarUrl: 'test.png' }; + await loginClient(client, user); + client.disconnect(); + }; + + const promises = []; + for (let i = 0; i < totalClients; i++) { + promises.push(testClient(i)); + } + + await Promise.all(promises); + }, 30000); + }); + + describe('Invalid Data Handling', () => { + test('should not crash when receiving invalid JSON', async () => { + const client = await createClient(); + const user = { name: 'invalidDataUser', avatarUrl: 'test.png' }; + + try { + await loginClient(client, user); + + client.emit('message', null, null, { invalid: 'data' }, null); + + await new Promise(resolve => setTimeout(resolve, 500)); + + expect(client.connected).toBe(true); + } finally { + client.disconnect(); + } + }); + + test('should handle large payload without crashing', async () => { + const client = await createClient(); + const user = { name: 'largePayloadUser', avatarUrl: 'test.png' }; + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + const largePayload = { data: 'x'.repeat(5000) }; + + try { + await loginClient(client, user); + + client.emit('message', user, to, largePayload, 'text'); + + await new Promise(resolve => setTimeout(resolve, 500)); + + expect(client.connected).toBe(true); + } finally { + client.disconnect(); + } + }); + }); +}); diff --git a/tests/frontend/chat-logic.test.js b/tests/frontend/chat-logic.test.js new file mode 100644 index 0000000..c5af03d --- /dev/null +++ b/tests/frontend/chat-logic.test.js @@ -0,0 +1,437 @@ +describe('Frontend - Chat Bubble Logic', () => { + describe('Message Display Logic', () => { + test('should determine if message is sent by current user', () => { + const loginUser = { id: 'user1', name: 'CurrentUser' }; + + const isSentByCurrentUser = (messageFromId) => { + return loginUser.id === messageFromId; + }; + + expect(isSentByCurrentUser('user1')).toBe(true); + expect(isSentByCurrentUser('user2')).toBe(false); + expect(isSentByCurrentUser(undefined)).toBe(false); + expect(isSentByCurrentUser(null)).toBe(false); + }); + + test('should handle different message types', () => { + const getMessageDisplayType = (message) => { + return message.type || 'text'; + }; + + const textMessage = { type: 'text', content: 'Hello' }; + const imageMessage = { type: 'image', content: 'base64data...' }; + const defaultMessage = { content: 'Default message' }; + + expect(getMessageDisplayType(textMessage)).toBe('text'); + expect(getMessageDisplayType(imageMessage)).toBe('image'); + expect(getMessageDisplayType(defaultMessage)).toBe('text'); + }); + + test('should handle image message placeholder', () => { + const renderImageContent = (content) => { + if (content === '【图片】') { + return { type: 'placeholder', text: '【图片】' }; + } + return { type: 'image', src: content }; + }; + + expect(renderImageContent('【图片】').type).toBe('placeholder'); + expect(renderImageContent('【图片】').text).toBe('【图片】'); + expect(renderImageContent('data:image/png;base64,abc').type).toBe('image'); + expect(renderImageContent('/assets/images/test.png').type).toBe('image'); + }); + }); + + describe('Message Bubble Styling', () => { + test('should apply correct CSS class based on sender', () => { + const getBubbleClass = (isSend) => { + return isSend ? 'chat-bubble-send' : 'chat-bubble-receive'; + }; + + expect(getBubbleClass(true)).toBe('chat-bubble-send'); + expect(getBubbleClass(false)).toBe('chat-bubble-receive'); + }); + + test('should show nickname based on setting', () => { + const shouldShowNickname = (setting) => { + return setting.isName !== false; + }; + + expect(shouldShowNickname({ isName: true })).toBe(true); + expect(shouldShowNickname({ isName: false })).toBe(false); + expect(shouldShowNickname({})).toBe(true); + }); + + test('should show time based on setting', () => { + const shouldShowTime = (setting) => { + return setting.isTime !== false; + }; + + expect(shouldShowTime({ isTime: true })).toBe(true); + expect(shouldShowTime({ isTime: false })).toBe(false); + expect(shouldShowTime({})).toBe(true); + }); + }); + + describe('Avatar Position', () => { + test('should position avatar on right for sent messages', () => { + const getAvatarPosition = (isSend) => { + return isSend ? 'right' : 'left'; + }; + + expect(getAvatarPosition(true)).toBe('right'); + expect(getAvatarPosition(false)).toBe('left'); + }); + }); +}); + +describe('Frontend - User List Logic', () => { + describe('Add User to List', () => { + test('should add new user to list', () => { + let users = [ + { id: 'group_001', name: '群聊天室', type: 'group' }, + { id: 'user1', name: 'User1', type: 'user' } + ]; + + const addUser = (user) => { + let index = -1; + for (let i = 0; i < users.length; i++) { + let item = users[i]; + if (user.id === item.id) { + index = i; + users[i] = user; + } + } + if (index === -1) { + users.push(user); + } + }; + + const newUser = { id: 'user2', name: 'User2', type: 'user' }; + addUser(newUser); + + expect(users.length).toBe(3); + expect(users[2].id).toBe('user2'); + expect(users[2].name).toBe('User2'); + }); + + test('should update existing user in list', () => { + let users = [ + { id: 'user1', name: 'User1', type: 'user', ip: '192.168.1.1' } + ]; + + const addUser = (user) => { + let index = -1; + for (let i = 0; i < users.length; i++) { + let item = users[i]; + if (user.id === item.id) { + index = i; + users[i] = user; + } + } + if (index === -1) { + users.push(user); + } + }; + + const updatedUser = { id: 'user1', name: 'User1', type: 'user', ip: '192.168.1.2' }; + addUser(updatedUser); + + expect(users.length).toBe(1); + expect(users[0].ip).toBe('192.168.1.2'); + }); + }); + + describe('Remove User from List', () => { + test('should remove user from list when logout', () => { + let users = [ + { id: 'group_001', name: '群聊天室', type: 'group' }, + { id: 'user1', name: 'User1', type: 'user' }, + { id: 'user2', name: 'User2', type: 'user' } + ]; + let curSession = { id: 'user2', name: 'User2' }; + + const removeUser = (user) => { + for (let i = 0; i < users.length; i++) { + let item = users[i]; + if (user.id === item.id) { + users.splice(i, 1); + if (item.id === curSession.id) { + curSession = {}; + } + break; + } + } + }; + + removeUser({ id: 'user1' }); + expect(users.length).toBe(2); + expect(users.find(u => u.id === 'user1')).toBeUndefined(); + + removeUser({ id: 'user2' }); + expect(users.length).toBe(1); + expect(curSession.id).toBeUndefined(); + }); + + test('should not modify list when user not found', () => { + let users = [ + { id: 'user1', name: 'User1', type: 'user' } + ]; + const originalLength = users.length; + + const removeUser = (user) => { + for (let i = 0; i < users.length; i++) { + let item = users[i]; + if (user.id === item.id) { + users.splice(i, 1); + break; + } + } + }; + + removeUser({ id: 'nonexistent' }); + expect(users.length).toBe(originalLength); + }); + }); + + describe('Search Users', () => { + test('should search users by name or id', () => { + const users = [ + { id: 'group_001', name: '群聊天室', type: 'group' }, + { id: 'alice_123', name: 'Alice Wang', type: 'user' }, + { id: 'bob_456', name: 'Bob Chen', type: 'user' }, + { id: 'charlie_789', name: 'Charlie Liu', type: 'user' } + ]; + + const searchUser = (keyword) => { + let arr = []; + users.forEach((item) => { + if ((item.name.indexOf(keyword) !== -1) || (item.id.indexOf(keyword) !== -1)) { + arr.push(item); + } + }); + return arr; + }; + + expect(searchUser('Alice').length).toBe(1); + expect(searchUser('Alice')[0].name).toBe('Alice Wang'); + + expect(searchUser('bob').length).toBe(1); + expect(searchUser('Chen').length).toBe(1); + + expect(searchUser('123').length).toBe(1); + expect(searchUser('123')[0].id).toBe('alice_123'); + + expect(searchUser('group').length).toBe(1); + + expect(searchUser('nonexistent').length).toBe(0); + + expect(searchUser('').length).toBe(4); + }); + }); + + describe('User List Display', () => { + test('should include group chat in user list', () => { + const users = [ + { id: 'group_001', name: '群聊天室', type: 'group' }, + { id: 'user1', name: 'User1', type: 'user' } + ]; + + const groupChat = users.find(u => u.type === 'group'); + expect(groupChat).toBeDefined(); + expect(groupChat.id).toBe('group_001'); + expect(groupChat.name).toBe('群聊天室'); + }); + + test('should distinguish between user and group types', () => { + const isGroup = (item) => item.type === 'group'; + const isUser = (item) => item.type === 'user'; + + expect(isGroup({ type: 'group' })).toBe(true); + expect(isGroup({ type: 'user' })).toBe(false); + + expect(isUser({ type: 'user' })).toBe(true); + expect(isUser({ type: 'group' })).toBe(false); + }); + }); +}); + +describe('Frontend - Session Management', () => { + describe('Change Session', () => { + test('should change current session', () => { + let curSession = {}; + + const changeSession = (session) => { + if (session.id === curSession.id) { + return; + } + curSession = session; + }; + + const groupSession = { id: 'group_001', name: '群聊天室' }; + changeSession(groupSession); + + expect(curSession.id).toBe('group_001'); + expect(curSession.name).toBe('群聊天室'); + + const userSession = { id: 'user1', name: 'User1' }; + changeSession(userSession); + + expect(curSession.id).toBe('user1'); + expect(curSession.name).toBe('User1'); + + const sameSession = { id: 'user1', name: 'User1' }; + changeSession(sameSession); + expect(curSession.id).toBe('user1'); + }); + + test('should clear session when session is empty', () => { + let curSession = { id: 'user1', name: 'User1' }; + + const changeSession = (session) => { + curSession = session; + }; + + changeSession({}); + + expect(curSession.id).toBeUndefined(); + }); + }); + + describe('Session Messages', () => { + test('should get messages for session', () => { + const messageData = { + 'group_001': [ + { id: 1, content: 'Hello Group' }, + { id: 2, content: 'Hi there' } + ], + 'user1': [ + { id: 3, content: 'Private message' } + ] + }; + + const getMessages = (sessionId) => { + if (sessionId && messageData[sessionId]) { + return messageData[sessionId]; + } + return []; + }; + + expect(getMessages('group_001').length).toBe(2); + expect(getMessages('user1').length).toBe(1); + expect(getMessages('nonexistent').length).toBe(0); + expect(getMessages('').length).toBe(0); + }); + + test('should add message to session', () => { + const messageData = { + 'group_001': [] + }; + + const addSessionMessage = (message, sessionId) => { + if (!messageData[sessionId]) { + messageData[sessionId] = []; + } + messageData[sessionId].push(message); + }; + + const message = { content: 'New message', time: Date.now() }; + addSessionMessage(message, 'group_001'); + + expect(messageData['group_001'].length).toBe(1); + expect(messageData['group_001'][0].content).toBe('New message'); + + addSessionMessage({ content: 'Another message' }, 'user2'); + expect(messageData['user2'].length).toBe(1); + }); + }); + + describe('Unread Messages', () => { + test('should count unread messages for session', () => { + const messageData = { + 'group_001': [ + { id: 1, content: 'Read message', isRead: true }, + { id: 2, content: 'Unread message 1', isRead: false }, + { id: 3, content: 'Unread message 2', isRead: false } + ] + }; + + const unReadNum = (sessionId) => { + let messages = []; + if (sessionId && messageData[sessionId]) { + messages = messageData[sessionId]; + } + let num = 0; + messages.forEach((item) => { + if (!item.isRead) { + num++; + } + }); + return num; + }; + + expect(unReadNum('group_001')).toBe(2); + expect(unReadNum('nonexistent')).toBe(0); + }); + + test('should mark messages as read when session is selected', () => { + const messageData = { + 'group_001': [ + { id: 1, isRead: false }, + { id: 2, isRead: false } + ] + }; + + const getMessages = (sessionId) => { + if (sessionId && messageData[sessionId]) { + return messageData[sessionId]; + } + return []; + }; + + const setSessionRead = (sessionId) => { + let messages = getMessages(sessionId); + if (messages.length === 0) { + return; + } + messages.forEach((item) => { + if (!item.isRead) { + item.isRead = true; + } + }); + }; + + setSessionRead('group_001'); + + expect(messageData['group_001'][0].isRead).toBe(true); + expect(messageData['group_001'][1].isRead).toBe(true); + }); + }); + + describe('Last Message Display', () => { + test('should get last message for session', () => { + const messageData = { + 'group_001': [ + { id: 1, content: 'First message', time: 1000 }, + { id: 2, content: 'Last message', time: 2000 } + ] + }; + + const finallyMessage = (sessionId) => { + let messages = []; + if (sessionId && messageData[sessionId]) { + messages = messageData[sessionId]; + } + const len = messages.length; + if (len > 0) { + return messages[len - 1]; + } else { + return {}; + } + }; + + expect(finallyMessage('group_001').content).toBe('Last message'); + expect(finallyMessage('nonexistent')).toEqual({}); + }); + }); +}); diff --git a/tests/frontend/filters.test.js b/tests/frontend/filters.test.js new file mode 100644 index 0000000..1f815d7 --- /dev/null +++ b/tests/frontend/filters.test.js @@ -0,0 +1,95 @@ +const { friendlyTime, formatTime } = require('../../src/components/filters'); + +describe('Frontend - Time Filters', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2026-04-19T12:00:00')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + describe('friendlyTime', () => { + test('should return "刚刚" for time within 3 minutes', () => { + const now = Date.now(); + const oneMinuteAgo = now - 60 * 1000; + const twoMinutesAgo = now - 2 * 60 * 1000; + + expect(friendlyTime(oneMinuteAgo)).toBe('刚刚'); + expect(friendlyTime(twoMinutesAgo)).toBe('刚刚'); + }); + + test('should return minutes ago for time between 3 minutes and 1 hour', () => { + const now = Date.now(); + const fiveMinutesAgo = now - 5 * 60 * 1000; + const thirtyMinutesAgo = now - 30 * 60 * 1000; + const fiftyNineMinutesAgo = now - 59 * 60 * 1000; + + expect(friendlyTime(fiveMinutesAgo)).toBe('5分钟前'); + expect(friendlyTime(thirtyMinutesAgo)).toBe('30分钟前'); + expect(friendlyTime(fiftyNineMinutesAgo)).toBe('59分钟前'); + }); + + test('should return hours ago for time between 1 hour and 24 hours', () => { + const now = Date.now(); + const oneHourAgo = now - 60 * 60 * 1000; + const twelveHoursAgo = now - 12 * 60 * 60 * 1000; + const twentyThreeHoursAgo = now - 23 * 60 * 60 * 1000; + + expect(friendlyTime(oneHourAgo)).toBe('1小时前'); + expect(friendlyTime(twelveHoursAgo)).toBe('12小时前'); + expect(friendlyTime(twentyThreeHoursAgo)).toBe('23小时前'); + }); + + test('should return days ago for time between 1 day and 3 days', () => { + const now = Date.now(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000; + + expect(friendlyTime(oneDayAgo)).toBe('1天前'); + expect(friendlyTime(twoDaysAgo)).toBe('2天前'); + }); + + test('should return formatted date for time more than 3 days', () => { + const april15 = new Date('2026-04-15T12:00:00').getTime(); + const january1 = new Date('2026-01-01T12:00:00').getTime(); + const december25 = new Date('2025-12-25T12:00:00').getTime(); + + expect(friendlyTime(april15)).toBe('2026.4.15'); + expect(friendlyTime(january1)).toBe('2026.1.1'); + expect(friendlyTime(december25)).toBe('2025.12.25'); + }); + }); + + describe('formatTime', () => { + test('should format time with full date and time', () => { + const testDate = new Date('2026-04-19T14:30:45').getTime(); + expect(formatTime(testDate)).toBe('2026.4.19 14:30:45'); + }); + + test('should pad single digit hours, minutes, seconds with leading zero', () => { + const earlyMorning = new Date('2026-01-05T09:05:08').getTime(); + const midnight = new Date('2026-12-31T00:00:00').getTime(); + + expect(formatTime(earlyMorning)).toBe('2026.1.5 09:05:08'); + expect(formatTime(midnight)).toBe('2026.12.31 00:00:00'); + }); + + test('should handle different months correctly', () => { + const march = new Date('2026-03-15T12:00:00').getTime(); + const october = new Date('2026-10-15T12:00:00').getTime(); + + expect(formatTime(march)).toBe('2026.3.15 12:00:00'); + expect(formatTime(october)).toBe('2026.10.15 12:00:00'); + }); + + test('should handle boundary values', () => { + const yearEnd = new Date('2026-12-31T23:59:59').getTime(); + const yearStart = new Date('2026-01-01T00:00:00').getTime(); + + expect(formatTime(yearEnd)).toBe('2026.12.31 23:59:59'); + expect(formatTime(yearStart)).toBe('2026.1.1 00:00:00'); + }); + }); +}); diff --git a/tests/frontend/message-panel.test.js b/tests/frontend/message-panel.test.js new file mode 100644 index 0000000..d9a95cb --- /dev/null +++ b/tests/frontend/message-panel.test.js @@ -0,0 +1,252 @@ +describe('Frontend - Message Panel Logic', () => { + describe('Text Input and Trimming', () => { + test('should trim whitespace from message before sending', () => { + const trimMessage = (text) => { + return text.replace(/^\s+|\s+$/g, ''); + }; + + expect(trimMessage(' hello world ')).toBe('hello world'); + expect(trimMessage('hello world')).toBe('hello world'); + expect(trimMessage(' ')).toBe(''); + expect(trimMessage('\nhello\n')).toBe('hello'); + expect(trimMessage('\thello\t')).toBe('hello'); + }); + + test('should not send empty message after trimming', () => { + const shouldSendMessage = (text) => { + const trimmed = text.replace(/^\s+|\s+$/g, ''); + return trimmed !== ''; + }; + + expect(shouldSendMessage('hello')).toBe(true); + expect(shouldSendMessage(' hello ')).toBe(true); + expect(shouldSendMessage('')).toBe(false); + expect(shouldSendMessage(' ')).toBe(false); + expect(shouldSendMessage('\n\n\n')).toBe(false); + }); + }); + + describe('Enter Key Send Message', () => { + test('should detect enter key press and send message', () => { + let messageSent = false; + let sentContent = ''; + + const sendText = (text) => { + const trimmed = text.replace(/^\s+|\s+$/g, ''); + if (trimmed) { + messageSent = true; + sentContent = trimmed; + return true; + } + return false; + }; + + const handleKeyPress = (event, currentText) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + return sendText(currentText); + } + return false; + }; + + let event = { key: 'Enter', shiftKey: false, preventDefault: jest.fn() }; + let result = handleKeyPress(event, 'Hello World'); + + expect(result).toBe(true); + expect(messageSent).toBe(true); + expect(sentContent).toBe('Hello World'); + expect(event.preventDefault).toHaveBeenCalled(); + + messageSent = false; + sentContent = ''; + event = { key: 'Enter', shiftKey: true, preventDefault: jest.fn() }; + result = handleKeyPress(event, 'Hello World'); + + expect(result).toBe(false); + expect(messageSent).toBe(false); + + messageSent = false; + sentContent = ''; + event = { key: 'Enter', shiftKey: false, preventDefault: jest.fn() }; + result = handleKeyPress(event, ' '); + + expect(result).toBe(false); + expect(messageSent).toBe(false); + }); + }); + + describe('Emoji Picker Logic', () => { + test('should append emoji title to text input', () => { + let text = ''; + + const pickerEmoji = (emoji) => { + text += emoji.title; + }; + + pickerEmoji({ title: '[微笑]', url: 'smile.gif' }); + expect(text).toBe('[微笑]'); + + pickerEmoji({ title: '[开心]', url: 'happy.gif' }); + expect(text).toBe('[微笑][开心]'); + + text = 'Hello '; + pickerEmoji({ title: '[哈哈]', url: 'laugh.gif' }); + expect(text).toBe('Hello [哈哈]'); + }); + }); + + describe('Toggle Emoji Panel', () => { + test('should toggle emoji panel visibility', () => { + let isShow = false; + let eventListenerAdded = false; + let hideCallback = null; + + const mockDocument = { + addEventListener: jest.fn((event, callback) => { + eventListenerAdded = true; + hideCallback = callback; + }), + removeEventListener: jest.fn(() => { + eventListenerAdded = false; + hideCallback = null; + }) + }; + + const toggleShow = () => { + const hide = () => { + isShow = false; + mockDocument.removeEventListener('click', hide); + }; + + if (isShow) { + hide(); + } else { + isShow = true; + mockDocument.addEventListener('click', hide); + } + }; + + toggleShow(); + expect(isShow).toBe(true); + expect(eventListenerAdded).toBe(true); + + toggleShow(); + expect(isShow).toBe(false); + expect(eventListenerAdded).toBe(false); + + toggleShow(); + expect(isShow).toBe(true); + expect(eventListenerAdded).toBe(true); + + if (hideCallback) { + hideCallback(); + } + expect(isShow).toBe(false); + expect(eventListenerAdded).toBe(false); + }); + }); + + describe('Clear Input After Send', () => { + test('should clear text input after sending message', () => { + jest.useFakeTimers(); + + let text = 'Hello World'; + + const sendText = (inputText) => { + const trimmed = inputText.replace(/^\s+|\s+$/g, ''); + if (trimmed) { + setTimeout(() => { + text = ''; + }, 0); + return true; + } + return false; + }; + + const result = sendText(text); + expect(result).toBe(true); + expect(text).toBe('Hello World'); + + jest.runAllTimers(); + expect(text).toBe(''); + + jest.useRealTimers(); + }); + }); +}); + +describe('Frontend - Image Upload Logic', () => { + describe('Image Validation', () => { + test('should validate image file extension', () => { + const validateImageExtension = (filename) => { + const reg = /\.(?:png|jpg|jpeg)$/i; + return reg.test(filename); + }; + + expect(validateImageExtension('test.png')).toBe(true); + expect(validateImageExtension('test.jpg')).toBe(true); + expect(validateImageExtension('test.jpeg')).toBe(true); + expect(validateImageExtension('test.PNG')).toBe(true); + expect(validateImageExtension('test.JPG')).toBe(true); + expect(validateImageExtension('test.JPEG')).toBe(true); + expect(validateImageExtension('image.test.png')).toBe(true); + + expect(validateImageExtension('test.gif')).toBe(false); + expect(validateImageExtension('test.bmp')).toBe(false); + expect(validateImageExtension('test.txt')).toBe(false); + expect(validateImageExtension('test')).toBe(false); + expect(validateImageExtension('')).toBe(false); + }); + + test('should validate image file size', () => { + const maxSize = 1 * 1024 * 1024; + + const validateImageSize = (fileSize) => { + return fileSize <= maxSize; + }; + + expect(validateImageSize(0)).toBe(true); + expect(validateImageSize(1024)).toBe(true); + expect(validateImageSize(500 * 1024)).toBe(true); + expect(validateImageSize(1024 * 1024)).toBe(true); + + expect(validateImageSize(1024 * 1024 + 1)).toBe(false); + expect(validateImageSize(2 * 1024 * 1024)).toBe(false); + expect(validateImageSize(10 * 1024 * 1024)).toBe(false); + }); + }); + + describe('FileReader Logic', () => { + test('should read file as base64', () => { + jest.useFakeTimers(); + + const mockFile = { + name: 'test.png', + size: 1024, + type: 'image/png' + }; + + const mockBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'; + let receivedResult = null; + + const readFileAsDataURL = (file, callback) => { + setTimeout(() => { + callback(mockBase64); + }, 10); + }; + + readFileAsDataURL(mockFile, (result) => { + receivedResult = result; + }); + + expect(receivedResult).toBeNull(); + + jest.runAllTimers(); + + expect(receivedResult).toBe(mockBase64); + expect(receivedResult).toMatch(/^data:image\/\w+;base64,/); + + jest.useRealTimers(); + }); + }); +}); diff --git a/tests/frontend/user-login.test.js b/tests/frontend/user-login.test.js new file mode 100644 index 0000000..525965c --- /dev/null +++ b/tests/frontend/user-login.test.js @@ -0,0 +1,141 @@ +describe('Frontend - UserLogin Component Logic', () => { + describe('Login Validation', () => { + test('should require non-empty username', () => { + const validateLogin = (user) => { + if (!user || !user.name || user.name.trim() === '') { + return { valid: false, error: '请输入用户名称!' }; + } + return { valid: true, error: null }; + }; + + expect(validateLogin({ name: '' }).valid).toBe(false); + expect(validateLogin({ name: '' }).error).toBe('请输入用户名称!'); + expect(validateLogin({ name: ' ' }).valid).toBe(false); + expect(validateLogin({ name: 'testUser' }).valid).toBe(true); + expect(validateLogin(null).valid).toBe(false); + }); + + test('should validate username length', () => { + const validateUsernameLength = (name) => { + if (name.length < 1) { + return { valid: false, error: '用户名不能为空' }; + } + if (name.length > 20) { + return { valid: false, error: '用户名长度不能超过20个字符' }; + } + return { valid: true, error: null }; + }; + + expect(validateUsernameLength('a').valid).toBe(true); + expect(validateUsernameLength('testUser').valid).toBe(true); + expect(validateUsernameLength('a'.repeat(20)).valid).toBe(true); + expect(validateUsernameLength('a'.repeat(21)).valid).toBe(false); + expect(validateUsernameLength('a'.repeat(21)).error).toContain('超过20'); + }); + + test('should validate QQ number format for avatar', () => { + const validateQQ = (qq) => { + const qqReg = /^[1-9][0-9]{3,9}[0-9]$/; + return qqReg.test(qq); + }; + + expect(validateQQ('12345')).toBe(true); + expect(validateQQ('705597001')).toBe(true); + expect(validateQQ('1000000000')).toBe(true); + + expect(validateQQ('01234')).toBe(false); + expect(validateQQ('123')).toBe(false); + expect(validateQQ('abcdef')).toBe(false); + expect(validateQQ('')).toBe(false); + }); + }); + + describe('Avatar Selection Logic', () => { + test('should select avatar from list', () => { + const avatarList = [ + 'avatar1.png', + 'avatar2.png', + 'avatar3.png' + ]; + + let selectedAvatar = avatarList[0]; + + selectedAvatar = avatarList[1]; + expect(selectedAvatar).toBe('avatar2.png'); + + selectedAvatar = avatarList[2]; + expect(selectedAvatar).toBe('avatar3.png'); + }); + + test('should add QQ avatar to list if not exists', () => { + let avatarList = ['avatar1.png']; + + const addQQAvatar = (qq) => { + const qqAvatarUrl = `http://q.qlogo.cn/headimg_dl?dst_uin=${qq}&spec=100`; + if (avatarList.indexOf(qqAvatarUrl) === -1) { + avatarList.push(qqAvatarUrl); + return true; + } + return false; + }; + + expect(addQQAvatar('12345')).toBe(true); + expect(avatarList.length).toBe(2); + expect(avatarList[1]).toContain('12345'); + + expect(addQQAvatar('12345')).toBe(false); + expect(avatarList.length).toBe(2); + }); + + test('should generate random avatar from list', () => { + const avatarList = [ + 'avatar1.png', + 'avatar2.png', + 'avatar3.png', + 'avatar4.png', + 'avatar5.png' + ]; + + const randomAvatar = () => { + const len = avatarList.length; + const index = Math.floor(Math.random() * len); + return avatarList[index]; + }; + + const selectedAvatars = new Set(); + for (let i = 0; i < 100; i++) { + selectedAvatars.add(randomAvatar()); + } + + expect(selectedAvatars.size).toBeGreaterThan(1); + selectedAvatars.forEach(avatar => { + expect(avatarList).toContain(avatar); + }); + }); + }); + + describe('Random Name Generation', () => { + test('should generate random name starting with "cname"', () => { + const randomName = () => { + return 'cname'; + }; + + expect(randomName()).toBe('cname'); + }); + + test('should generate random QQ number', () => { + const randomQQ = () => { + const min = 100000; + const max = 1000000000; + return parseInt(Math.random() * (max - min + 1) + min, 10); + }; + + for (let i = 0; i < 10; i++) { + const qq = randomQQ(); + expect(qq).toBeGreaterThanOrEqual(100000); + expect(qq).toBeLessThanOrEqual(1000000000); + expect(Number.isInteger(qq)).toBe(true); + } + }); + }); +}); diff --git a/tests/integration/socket.test.js b/tests/integration/socket.test.js new file mode 100644 index 0000000..f324366 --- /dev/null +++ b/tests/integration/socket.test.js @@ -0,0 +1,255 @@ +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const Client = require('socket.io-client'); +const jwt = require('../../server/jwt'); + +describe('Integration Tests - Socket.IO Chat', () => { + let io; + let httpServer; + const PORT = 3001; + + beforeAll((done) => { + httpServer = createServer(); + io = new Server(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST'] + } + }); + + io.on('connection', (socket) => { + socket.on('login', (user) => { + const sockets = io.sockets.sockets; + let nameExists = false; + + sockets.forEach((s) => { + if (s.user && s.user.name === user.name) { + nameExists = true; + } + }); + + if (nameExists) { + socket.emit('loginFail', '登录失败,昵称已存在!'); + return; + } + + user.id = socket.id; + user.time = new Date().getTime(); + user.type = 'user'; + socket.user = user; + + socket.broadcast.emit('system', user, 'join'); + socket.emit('loginSuccess', { user, token: jwt.token(user) }, [ + { id: 'group_001', name: '群聊天室', type: 'group' } + ]); + + socket.on('message', (from, to, message, type) => { + if (!socket.user) { + socket.emit('error', '请先登录!'); + return; + } + + if (to.type === 'group') { + socket.broadcast.emit('message', socket.user, to, message, type); + } + }); + + socket.on('disconnect', () => { + if (socket.user) { + socket.broadcast.emit('system', socket.user, 'logout'); + } + }); + }); + }); + + httpServer.listen(PORT, done); + }); + + afterAll((done) => { + if (io) { + io.close(); + } + if (httpServer) { + httpServer.close(done); + } else { + done(); + } + }); + + const createClient = () => { + return new Promise((resolve) => { + const client = Client(`http://localhost:${PORT}`, { + transports: ['websocket'], + reconnection: false + }); + client.on('connect', () => resolve(client)); + }); + }; + + const loginClient = (client, user) => { + return new Promise((resolve, reject) => { + client.once('loginSuccess', (data, users) => { + resolve({ data, users }); + }); + client.once('loginFail', (message) => { + reject(new Error(message)); + }); + client.emit('login', user); + }); + }; + + describe('User Login', () => { + test('should login successfully with unique username', async () => { + const client = await createClient(); + const user = { name: 'testUser1', avatarUrl: 'test.png' }; + + try { + const result = await loginClient(client, user); + expect(result.data).toBeDefined(); + expect(result.data.user.name).toBe('testUser1'); + expect(result.data.token).toBeDefined(); + expect(result.users).toBeInstanceOf(Array); + } finally { + client.disconnect(); + } + }); + + test('should fail login with duplicate username', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + const user = { name: 'testUser2', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user); + + await expect(loginClient(client2, user)).rejects.toThrow('昵称已存在'); + } finally { + client1.disconnect(); + client2.disconnect(); + } + }); + }); + + describe('Message Broadcasting', () => { + test('should broadcast message to all clients in group', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + + const user1 = { name: 'senderUser', avatarUrl: 'test.png' }; + const user2 = { name: 'receiverUser', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user1); + await loginClient(client2, user2); + + const messageContent = 'Hello, this is a test message!'; + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + + const messagePromise = new Promise((resolve) => { + client2.once('message', (sender, recipient, message, type) => { + resolve({ sender, recipient, message, type }); + }); + }); + + client1.emit('message', user1, to, messageContent, 'text'); + + const result = await messagePromise; + + expect(result.sender.name).toBe('senderUser'); + expect(result.recipient.id).toBe('group_001'); + expect(result.message).toBe(messageContent); + expect(result.type).toBe('text'); + } finally { + client1.disconnect(); + client2.disconnect(); + } + }); + }); + + describe('System Messages (Join/Leave)', () => { + test('should broadcast system message when user joins', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + + const user1 = { name: 'existingUser', avatarUrl: 'test.png' }; + const user2 = { name: 'newUser', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user1); + + const systemMessagePromise = new Promise((resolve) => { + client1.once('system', (user, action) => { + resolve({ user, action }); + }); + }); + + await loginClient(client2, user2); + + const result = await systemMessagePromise; + + expect(result.user.name).toBe('newUser'); + expect(result.action).toBe('join'); + } finally { + client1.disconnect(); + client2.disconnect(); + } + }); + + test('should broadcast system message when user leaves', async () => { + const client1 = await createClient(); + const client2 = await createClient(); + + const user1 = { name: 'observerUser', avatarUrl: 'test.png' }; + const user2 = { name: 'leavingUser', avatarUrl: 'test.png' }; + + try { + await loginClient(client1, user1); + await loginClient(client2, user2); + + const systemMessagePromise = new Promise((resolve) => { + client1.once('system', (user, action) => { + resolve({ user, action }); + }); + }); + + client2.disconnect(); + + const result = await systemMessagePromise; + + expect(result.user.name).toBe('leavingUser'); + expect(result.action).toBe('logout'); + } finally { + client1.disconnect(); + } + }, 10000); + }); + + describe('Multiple Clients', () => { + test('should handle multiple concurrent clients', async () => { + const clientCount = 5; + const clients = []; + const loginPromises = []; + + for (let i = 0; i < clientCount; i++) { + const client = await createClient(); + clients.push(client); + + const user = { name: `multiUser${Date.now()}_${i}`, avatarUrl: 'test.png' }; + loginPromises.push(loginClient(client, user)); + } + + try { + const results = await Promise.all(loginPromises); + + expect(results.length).toBe(clientCount); + + results.forEach(result => { + expect(result.data.user.name).toBeDefined(); + expect(result.data.token).toBeDefined(); + }); + } finally { + clients.forEach(c => c.disconnect()); + } + }, 15000); + }); +}); diff --git a/tests/performance/performance.test.js b/tests/performance/performance.test.js new file mode 100644 index 0000000..17fac90 --- /dev/null +++ b/tests/performance/performance.test.js @@ -0,0 +1,337 @@ +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const Client = require('socket.io-client'); +const jwt = require('../../server/jwt'); + +describe('Performance Tests - Socket.IO Chat', () => { + let io; + let httpServer; + const PORT = 3003; + + beforeAll((done) => { + httpServer = createServer(); + io = new Server(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST'] + }, + maxHttpBufferSize: 1e6 + }); + + io.on('connection', (socket) => { + socket.on('login', (user) => { + const sockets = io.sockets.sockets; + let nameExists = false; + + sockets.forEach((s) => { + if (s.user && s.user.name === user.name) { + nameExists = true; + } + }); + + if (nameExists) { + socket.emit('loginFail', '登录失败,昵称已存在!'); + return; + } + + user.id = socket.id; + user.time = new Date().getTime(); + user.type = 'user'; + socket.user = user; + + socket.broadcast.emit('system', user, 'join'); + socket.emit('loginSuccess', { user, token: jwt.token(user) }, [ + { id: 'group_001', name: '群聊天室', type: 'group' } + ]); + + socket.on('message', (from, to, message, type) => { + if (to && to.type === 'group') { + socket.broadcast.emit('message', socket.user, to, message, type); + } + }); + + socket.on('disconnect', () => { + if (socket.user) { + socket.broadcast.emit('system', socket.user, 'logout'); + } + }); + }); + }); + + httpServer.listen(PORT, done); + }); + + afterAll((done) => { + if (io) { + io.close(); + } + if (httpServer) { + httpServer.close(done); + } else { + done(); + } + }); + + const createClient = () => { + return new Promise((resolve) => { + const client = Client(`http://localhost:${PORT}`, { + transports: ['websocket'], + reconnection: false + }); + client.on('connect', () => resolve(client)); + }); + }; + + const loginClient = (client, user) => { + return new Promise((resolve, reject) => { + client.once('loginSuccess', (data, users) => { + resolve({ data, users }); + }); + client.once('loginFail', (message) => { + reject(new Error(message)); + }); + client.emit('login', user); + }); + }; + + describe('Concurrent User Connections', () => { + test('should handle 20 concurrent users', async () => { + const userCount = 20; + const clients = []; + const loginPromises = []; + + const startTime = Date.now(); + + for (let i = 0; i < userCount; i++) { + const client = await createClient(); + clients.push(client); + + const user = { + name: `perfUser${Date.now()}_${i}`, + avatarUrl: `avatar${i}.png` + }; + loginPromises.push(loginClient(client, user)); + } + + const results = await Promise.all(loginPromises); + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(results.length).toBe(userCount); + expect(duration).toBeLessThan(10000); + + clients.forEach(c => c.disconnect()); + }, 30000); + + test('should handle 50 concurrent users', async () => { + const userCount = 50; + const clients = []; + const loginPromises = []; + + const startTime = Date.now(); + + for (let i = 0; i < userCount; i++) { + const client = await createClient(); + clients.push(client); + + const user = { + name: `perfUser50_${Date.now()}_${i}`, + avatarUrl: `avatar${i}.png` + }; + loginPromises.push(loginClient(client, user)); + } + + const results = await Promise.all(loginPromises); + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(results.length).toBe(userCount); + expect(duration).toBeLessThan(20000); + + clients.forEach(c => c.disconnect()); + }, 60000); + }); + + describe('Message Throughput', () => { + test('should handle rapid message sending', async () => { + const sender = await createClient(); + const receiver1 = await createClient(); + const receiver2 = await createClient(); + + const user1 = { name: 'senderPerf', avatarUrl: 'test.png' }; + const user2 = { name: 'receiverPerf1', avatarUrl: 'test.png' }; + const user3 = { name: 'receiverPerf2', avatarUrl: 'test.png' }; + + await loginClient(sender, user1); + await loginClient(receiver1, user2); + await loginClient(receiver2, user3); + + const messageCount = 50; + let receivedMessages = 0; + + const messagePromise = new Promise((resolve) => { + const checkReceived = () => { + if (receivedMessages >= messageCount * 2) { + resolve(); + } + }; + + receiver1.on('message', () => { + receivedMessages++; + checkReceived(); + }); + + receiver2.on('message', () => { + receivedMessages++; + checkReceived(); + }); + }); + + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + const startTime = Date.now(); + + for (let i = 0; i < messageCount; i++) { + sender.emit('message', user1, to, `Message ${i}`, 'text'); + } + + await messagePromise; + + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(receivedMessages).toBe(messageCount * 2); + expect(duration).toBeLessThan(5000); + + sender.disconnect(); + receiver1.disconnect(); + receiver2.disconnect(); + }, 20000); + + test('should not lose messages under load', async () => { + const clients = []; + const totalMessages = 100; + const expectedMessages = totalMessages * 4; + let receivedMessages = 0; + + const clientNames = ['loadTester1', 'loadTester2', 'loadTester3', 'loadTester4', 'loadTester5']; + const clientUsers = []; + + for (let i = 0; i < clientNames.length; i++) { + const client = await createClient(); + clients.push(client); + const user = { name: `${clientNames[i]}_${Date.now()}`, avatarUrl: 'test.png' }; + clientUsers.push(user); + await loginClient(client, user); + } + + const messagePromise = new Promise((resolve) => { + const checkReceived = () => { + if (receivedMessages >= expectedMessages) { + resolve(); + } + }; + + for (let i = 1; i < clients.length; i++) { + clients[i].on('message', () => { + receivedMessages++; + checkReceived(); + }); + } + }); + + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + const sender = clients[0]; + const senderUser = clientUsers[0]; + + const startTime = Date.now(); + + for (let i = 0; i < totalMessages; i++) { + sender.emit('message', senderUser, to, `Load test message ${i}`, 'text'); + } + + await messagePromise; + + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(receivedMessages).toBe(expectedMessages); + expect(duration).toBeLessThan(10000); + + clients.forEach(c => c.disconnect()); + }, 30000); + }); + + describe('Response Time', () => { + test('should have fast login response', async () => { + const responseTimes = []; + const testCount = 10; + + for (let i = 0; i < testCount; i++) { + const client = await createClient(); + const user = { + name: `responseTest${Date.now()}_${i}`, + avatarUrl: 'test.png' + }; + + const startTime = Date.now(); + await loginClient(client, user); + const endTime = Date.now(); + + responseTimes.push(endTime - startTime); + client.disconnect(); + } + + const avgTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length; + const maxTime = Math.max(...responseTimes); + + expect(avgTime).toBeLessThan(1000); + expect(maxTime).toBeLessThan(3000); + }, 30000); + + test('should have fast message delivery', async () => { + const sender = await createClient(); + const receiver = await createClient(); + + const senderUser = { name: 'speedSender', avatarUrl: 'test.png' }; + const receiverUser = { name: 'speedReceiver', avatarUrl: 'test.png' }; + + await loginClient(sender, senderUser); + await loginClient(receiver, receiverUser); + + const to = { id: 'group_001', name: '群聊天室', type: 'group' }; + const testCount = 20; + const deliveryTimes = []; + + const messagePromise = new Promise((resolve) => { + let testsCompleted = 0; + + receiver.on('message', (from, to, message, type) => { + const endTime = Date.now(); + const sendTime = parseInt(message.split('_')[1]); + deliveryTimes.push(endTime - sendTime); + testsCompleted++; + + if (testsCompleted === testCount) { + resolve(); + } + }); + }); + + for (let i = 0; i < testCount; i++) { + const sendTime = Date.now(); + sender.emit('message', senderUser, to, `speedTest_${sendTime}`, 'text'); + } + + await messagePromise; + + const avgTime = deliveryTimes.reduce((a, b) => a + b, 0) / deliveryTimes.length; + const maxTime = Math.max(...deliveryTimes); + + expect(avgTime).toBeLessThan(500); + expect(maxTime).toBeLessThan(2000); + + sender.disconnect(); + receiver.disconnect(); + }, 20000); + }); +}); diff --git a/tests/unit/filters.test.js b/tests/unit/filters.test.js new file mode 100644 index 0000000..4b37111 --- /dev/null +++ b/tests/unit/filters.test.js @@ -0,0 +1,74 @@ +const { friendlyTime, formatTime } = require('../../src/components/filters'); + +describe('Filters - friendlyTime', () => { + beforeEach(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2026-04-19T12:00:00')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('should return "刚刚" for time within 3 minutes', () => { + const now = new Date().getTime(); + const oneMinuteAgo = now - 60 * 1000; + + expect(friendlyTime(oneMinuteAgo)).toBe('刚刚'); + }); + + test('should return minutes ago for time between 3 minutes and 1 hour', () => { + const now = new Date().getTime(); + const fiveMinutesAgo = now - 5 * 60 * 1000; + const thirtyMinutesAgo = now - 30 * 60 * 1000; + + expect(friendlyTime(fiveMinutesAgo)).toBe('5分钟前'); + expect(friendlyTime(thirtyMinutesAgo)).toBe('30分钟前'); + }); + + test('should return hours ago for time between 1 hour and 24 hours', () => { + const now = new Date().getTime(); + const twoHoursAgo = now - 2 * 60 * 60 * 1000; + const twelveHoursAgo = now - 12 * 60 * 60 * 1000; + + expect(friendlyTime(twoHoursAgo)).toBe('2小时前'); + expect(friendlyTime(twelveHoursAgo)).toBe('12小时前'); + }); + + test('should return days ago for time between 1 day and 3 days', () => { + const now = new Date().getTime(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000; + + expect(friendlyTime(oneDayAgo)).toBe('1天前'); + expect(friendlyTime(twoDaysAgo)).toBe('2天前'); + }); + + test('should return formatted date for time more than 3 days', () => { + const date = new Date('2026-04-15T12:00:00'); + + expect(friendlyTime(date.getTime())).toBe('2026.4.15'); + }); +}); + +describe('Filters - formatTime', () => { + test('should format time correctly', () => { + const date = new Date('2026-04-19T14:30:45'); + + expect(formatTime(date.getTime())).toBe('2026.4.19 14:30:45'); + }); + + test('should pad single digit hours, minutes, seconds', () => { + const date = new Date('2026-01-05T09:05:08'); + + expect(formatTime(date.getTime())).toBe('2026.1.5 09:05:08'); + }); + + test('should handle different date formats', () => { + const date1 = new Date('2025-12-31T23:59:59'); + const date2 = new Date('2024-06-15T00:00:00'); + + expect(formatTime(date1.getTime())).toBe('2025.12.31 23:59:59'); + expect(formatTime(date2.getTime())).toBe('2024.6.15 00:00:00'); + }); +}); diff --git a/tests/unit/utils.test.js b/tests/unit/utils.test.js new file mode 100644 index 0000000..9a3b1b0 --- /dev/null +++ b/tests/unit/utils.test.js @@ -0,0 +1,162 @@ +const crypto = require('crypto'); +const util = require('../../server/utils'); + +describe('Utils - guid', () => { + test('should generate unique GUIDs', () => { + const guid1 = util.guid(); + const guid2 = util.guid(); + + expect(guid1).toBeDefined(); + expect(guid2).toBeDefined(); + expect(guid1).not.toBe(guid2); + }); + + test('should generate GUID with correct format', () => { + const guid = util.guid(); + const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + expect(guid).toMatch(guidPattern); + }); +}); + +describe('Utils - MD5', () => { + test('should generate MD5 hash', () => { + const text = 'hello world'; + const expectedHash = '5eb63bbbe01eeed093cb22bb8f5acdc3'; + const hash = util.MD5(text); + + expect(hash).toBe(expectedHash); + }); + + test('should generate same hash for same input', () => { + const text = 'test string'; + const hash1 = util.MD5(text); + const hash2 = util.MD5(text); + + expect(hash1).toBe(hash2); + }); + + test('should generate different hash for different input', () => { + const text1 = 'test string 1'; + const text2 = 'test string 2'; + const hash1 = util.MD5(text1); + const hash2 = util.MD5(text2); + + expect(hash1).not.toBe(hash2); + }); +}); + +describe('Utils - toHump and toLine', () => { + test('should convert underscore to camelCase', () => { + expect(util.toHump('user_name')).toBe('userName'); + expect(util.toHump('hello_world_test')).toBe('helloWorldTest'); + expect(util.toHump('alreadyCamel')).toBe('alreadyCamel'); + }); + + test('should convert camelCase to underscore', () => { + expect(util.toLine('userName')).toBe('user_name'); + expect(util.toLine('helloWorldTest')).toBe('hello_world_test'); + expect(util.toLine('already_underscore')).toBe('already_underscore'); + }); +}); + +describe('Utils - AES encrypt/decrypt', () => { + test('should encrypt and decrypt text correctly using modern API', () => { + const algorithm = 'aes-256-cbc'; + const key = crypto.scryptSync('test-key-123', 'salt', 32); + const iv = crypto.randomBytes(16); + const text = 'Hello, this is a secret message!'; + + const cipher = crypto.createCipheriv(algorithm, key, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + expect(encrypted).not.toBe(text); + expect(decrypted).toBe(text); + }); + + test('should throw error when decrypting with wrong key using modern API', () => { + const algorithm = 'aes-256-cbc'; + const key1 = crypto.scryptSync('test-key-123', 'salt', 32); + const key2 = crypto.scryptSync('wrong-key-456', 'salt', 32); + const iv = crypto.randomBytes(16); + const text = 'test message'; + + const cipher = crypto.createCipheriv(algorithm, key1, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + expect(() => { + const decipher = crypto.createDecipheriv(algorithm, key2, iv); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + }).toThrow(); + }); +}); + +describe('Utils - RSA generateKey', () => { + test('should generate RSA key pair', () => { + const keys = util.generateKey(); + + expect(keys).toBeDefined(); + expect(keys.publicKey).toBeDefined(); + expect(keys.privateKey).toBeDefined(); + expect(typeof keys.publicKey).toBe('string'); + expect(typeof keys.privateKey).toBe('string'); + expect(keys.publicKey).toContain('BEGIN PUBLIC KEY'); + expect(keys.privateKey).toContain('BEGIN PRIVATE KEY'); + }); +}); + +describe('Utils - RSA encrypt/decrypt', () => { + test('should encrypt and decrypt with RSA keys', () => { + const keys = util.generateKey(); + const text = 'Secret message for RSA test'; + + const encrypted = util.pubEncrypt(keys.publicKey, text); + const decrypted = util.priDecrypt(keys.privateKey, encrypted); + + expect(encrypted).not.toBe(text); + expect(decrypted.toString()).toBe(text); + }); +}); + +describe('Utils - generateTree', () => { + test('should generate tree structure from flat array', () => { + const items = [ + { id: 1, pid: null, name: 'Root' }, + { id: 2, pid: 1, name: 'Child 1' }, + { id: 3, pid: 1, name: 'Child 2' }, + { id: 4, pid: 2, name: 'Grandchild' } + ]; + + const tree = util.generateTree(items); + + expect(tree.length).toBe(1); + expect(tree[0].id).toBe(1); + expect(tree[0].children).toBeDefined(); + expect(tree[0].children.length).toBe(2); + + const child1 = tree[0].children.find(c => c.id === 2); + expect(child1).toBeDefined(); + expect(child1.children).toBeDefined(); + expect(child1.children.length).toBe(1); + expect(child1.children[0].id).toBe(4); + }); +}); + +describe('Utils - getNetworkIPv4', () => { + test('should get network IPv4 address', () => { + const result = util.getNetworkIPv4(); + + if (result) { + expect(typeof result).toBe('object'); + expect(result.family).toBe('IPv4'); + expect(result.internal).toBe(false); + } + }); +});