Conversation
…ntend utilities, config, and server API.
There was a problem hiding this comment.
Pull request overview
This PR introduces a Vitest-based test suite (frontend + server), refactors the Express server into a shared core to reuse between browser and Electron modes, and adds several security/performance improvements (XSS hardening, resource cleanup, scroll handler throttling), plus an Electron custom titlebar and local vendoring of some libraries.
Changes:
- Add Vitest + Supertest + JSDOM testing infrastructure with new unit/integration tests and CI workflow.
- Refactor server logic into
shared/server-core.jsand update browser/Electron entrypoints to reuse it. - Apply multiple security/perf fixes across UI rendering, bookmark storage, PDF/TXT rendering, and Electron window controls.
Reviewed changes
Copilot reviewed 29 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.js | Adds Vitest configuration (timeouts, include globs, environment mapping). |
| tests/server/utils.test.js | Adds unit tests for server path/extension utilities and security behaviors. |
| tests/server/api.test.js | Adds Supertest integration tests for server APIs (books/config/fonts/cover). |
| tests/frontend/utils.test.js | Adds unit tests for core frontend utilities. |
| tests/frontend/configManager.test.js | Adds regression test around safe bookmark rendering during config import. |
| tests/frontend/config.test.js | Adds unit tests for CONFIG constants and getFileKey. |
| tests/frontend/bookmarkStorage.test.js | Adds tests for canonical + legacy bookmark storage merging/migration. |
| tests/frontend/addBooksModal.test.js | Adds XSS safety test for add-books modal filename rendering. |
| src/vendor/jszip.min.js | Vendors JSZip locally for offline/packaged usage. |
| src/js/modules/txtReader.js | Escapes TXT chapter title before injecting into HTML to prevent XSS. |
| src/js/modules/pdfReader.js | Switches PDF.js loading to local vendor path; improves lifecycle reset for observers/binding. |
| src/js/modules/fileManager.js | Hardens bookshelf rendering against XSS by using DOM APIs; formatting cleanup. |
| src/js/modules/configManager.js | Migrates bookmark import/export to canonical storage helpers; URL-encodes download filename. |
| src/js/modules/bookmarkStorage.js | Introduces canonical bookmark map storage + legacy key migration helpers. |
| src/js/modules/bookmarkManager.js | Uses canonical bookmark storage, adds numeric timestamps, and applies HTML escaping in render. |
| src/js/modules/addBooksModal.js | Reworks file list rendering to DOM APIs to avoid HTML injection and improve safety. |
| src/js/bookshelfApp.js | Revokes evicted/replaced cover ObjectURLs to prevent memory leaks. |
| src/js/app.js | Refactors save logic, improves scroll progress handling (rAF throttling + DOM caching), improves unload persistence. |
| src/css/titlebar.css | Adds Electron-only custom titlebar styling and layout offsets. |
| shared/server-core.js | New shared Express core for browser + Electron modes, including security path checks and APIs. |
| server.js | Refactors to use shared server core; adds static caching and exports for tests. |
| scripts/launch-electron.js | Adds a cross-platform Electron launcher that clears problematic env vars. |
| reader.html | Uses locally vendored libs; adds Electron custom titlebar markup/logic. |
| preload.js | Exposes window control APIs and maximize state events to renderer. |
| package.json | Adds vitest/jsdom/supertest, updates scripts for testing and Electron launch. |
| index.html | Adds Electron custom titlebar markup/logic and CSS include. |
| electron-main.js | Uses shared server core, switches to frameless window, adds IPC for window controls + maximize events. |
| _test_electron.js | Adds a minimal Electron environment diagnostic script. |
| README.md | Documents the new test setup, structure, and commands. |
| .github/workflows/ci.yml | Adds CI workflow running npm test on Node 18/20. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app.delete('/api/book', (req, res) => { | ||
| try { | ||
| const relPath = req.query.path; | ||
| if (!relPath) { | ||
| return res.status(400).json({ error: '缺少书籍路径' }); | ||
| } | ||
| const absolutePath = utils.resolveBookPath(relPath); | ||
| if (!fs.existsSync(absolutePath)) { | ||
| return res.status(404).json({ error: '书籍不存在' }); | ||
| } | ||
| fs.unlinkSync(absolutePath); | ||
| utils.cleanupEmptyFolders(absolutePath); | ||
| coverCache.delete(absolutePath); | ||
| res.json({ success: true }); | ||
| } catch (error) { | ||
| console.error('Error deleting book:', error); | ||
| res.status(500).json({ error: '删除书籍失败: ' + error.message }); | ||
| } |
There was a problem hiding this comment.
DELETE /api/book 在 resolveBookPath 抛错(例如路径穿越/非法绝对路径)时会被外层 catch 捕获并返回 500,同时把 error.message 拼进响应。对非法输入更合适的响应是 403/400,并且不应在响应体里回显内部错误细节。建议在调用 resolveBookPath 的地方单独捕获并返回 403(或复用与 GET /api/book 一致的 Forbidden 行为)。
| npm test | ||
|
|
||
| # 运行测试并显示覆盖率 | ||
| npm run test:coverage |
There was a problem hiding this comment.
README 中给出了 npm run test:coverage 的运行方式,但 package.json 的 scripts 里并没有 test:coverage,会导致文档指引无法执行。建议补充对应脚本(例如 vitest run --coverage)或移除/改写该段说明。
| npm run test:coverage | |
| npx vitest run --coverage |
| describe('POST /api/upload — 上传书籍', () => { | ||
| const testFilePath = path.join(__dirname, 'test-upload.txt'); | ||
|
|
||
| beforeEach(() => { |
There was a problem hiding this comment.
此测试文件使用 ESM(顶部 import ... + createRequire(import.meta.url)),但同时在多处使用了 __dirname(例如构造 test-upload.txt / test.xyz 路径)。在 ESM 环境下 __dirname 未定义,会导致用例直接崩溃。建议改为用 fileURLToPath(import.meta.url)/path.dirname(...) 生成当前目录路径,或改成 CommonJS 写法保持一致。
| // BUG-4: HTML 转义函数,防止 XSS | ||
| const escHtml = (str) => String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | ||
|
|
||
| sortedBookmarks.forEach(bookmark => { | ||
| const el = document.createElement('div'); | ||
| const lvl = bookmark.level || 1; | ||
| el.className = 'bookmark-item level-' + lvl; | ||
| el.style.paddingLeft = ((lvl - 1) * 12) + 'px'; | ||
| el.innerHTML = ` | ||
| <div class="bookmark-title"><span style="display:inline-block;min-width:28px;padding:2px 6px;margin-right:6px;border-radius:10px;font-size:12px;opacity:.7;background:rgba(127,127,127,.15);">Lv${lvl}</span>${bookmark.title}</div> | ||
| <div class="bookmark-location">${bookmark.location.chapterTitle}</div> | ||
| <div class="bookmark-time">${bookmark.createdAt}</div> | ||
| <button class="bookmark-delete" onclick="window.removeBookmark('${bookmark.id}')" title="删除书签">×</button> | ||
| <div class="bookmark-title"><span style="display:inline-block;min-width:28px;padding:2px 6px;margin-right:6px;border-radius:10px;font-size:12px;opacity:.7;background:rgba(127,127,127,.15);">Lv${lvl}</span>${escHtml(bookmark.title)}</div> | ||
| <div class="bookmark-location">${escHtml(bookmark.location.chapterTitle)}</div> | ||
| <div class="bookmark-time">${escHtml(bookmark.createdAt)}</div> | ||
| <button class="bookmark-delete" onclick="window.removeBookmark('${escHtml(bookmark.id)}')" title="删除书签">×</button> |
There was a problem hiding this comment.
这里用 innerHTML + 内联 onclick="window.removeBookmark('...')" 拼接 bookmark.id。当前 escHtml 只转义了 & < > ",没有处理单引号 ',因此导入的书签 id 若包含 ' 会破坏属性边界并形成 XSS 注入面。建议避免内联事件(改为 addEventListener + data-*),或至少补全对 ' 的转义。
No description provided.