- chore: 升级 @metona-team/metona-sqlark 0.4.4 → 0.7.4,存储后端迁移 KVStore 引擎(内存索引+快照/日志,OPFS 落盘,jsdom 回退 memory), 启动时自动删除旧 IndexedDB 库(aria-MarkLiteV2 / Dexie MarkLite), 不做向下兼容 - fix: 保存竞态 — 快照比对后才清 isModified,防止保存期间的新输入被 误清标记导致永不落盘(自动保存与手动保存均修复) - fix: 另存为后标签重绑新路径(此前 Ctrl+S/自动保存仍写回旧文件), 并同步最近文件与快照持久化 - fix: tabSwitched 只依赖活动文件路径(此前每次击键都触发 IPC 并 重启主进程文件 watcher) - fix: 主进程关闭兜底超时 5s→12s(长于 confirm 10s,防强杀丢编辑) - fix: 外部修改检测覆盖非活动标签,banner 显示文件名, 标签有未保存修改时显式提示不再静默 - fix: 导入备份后 flush + closeDatabase 再 reload(防 OPFS 未落盘丢数据), 导入整体事务原子化(防半导入状态) - fix: 文档大纲跳过代码块内标题;切换标签恢复光标位置 - fix: 首次启动主题跟随系统偏好(settings 无记录时 load 返回 null) - fix: watcher error 恢复时重置 isSelfWriting,防外部修改通知被永久吞掉 - fix: 打开失败路径(最近文件/文件树/打开对话框)显式提示 - fix: 保存时保留原文件编码(UTF-16 LE/BE BOM 同编码写回) - feat: 多文件搜索 — dir:search IPC + SearchPanel 弹层 + Ctrl+Shift+F, 文件夹内递归搜索(大小写/正则,结果定位到行) - feat: 标签恢复时与磁盘 mtime 比对,自动同步磁盘最新内容 - test: 新增 outlineUtils(6) / searchInDir(7) / updateTabFilePath(2) 测试 - docs: README/DESIGN 同步 kv 后端与版本号 v0.6.2
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
// @vitest-environment node
|
||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs'
|
||
import { tmpdir } from 'os'
|
||
import { join } from 'path'
|
||
import { searchInDir } from '../file-system'
|
||
|
||
describe('searchInDir (主进程多文件搜索)', () => {
|
||
let dir: string
|
||
|
||
beforeAll(() => {
|
||
dir = mkdtempSync(join(tmpdir(), 'marklite-search-'))
|
||
mkdirSync(join(dir, 'sub'))
|
||
mkdirSync(join(dir, 'node_modules'))
|
||
writeFileSync(join(dir, 'a.md'), '# Hello\nline two\nWorld hello again\n')
|
||
writeFileSync(join(dir, 'sub', 'b.markdown'), 'nothing here\nHELLO UPPER\n')
|
||
writeFileSync(join(dir, 'skip.js'), 'hello in js\n')
|
||
writeFileSync(join(dir, 'node_modules', 'c.md'), 'hello in deps\n')
|
||
writeFileSync(join(dir, '.hidden.md'), 'hello hidden\n')
|
||
})
|
||
|
||
afterAll(() => {
|
||
rmSync(dir, { recursive: true, force: true })
|
||
})
|
||
|
||
it('should find matches across files (case-insensitive default)', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: 'hello' })
|
||
expect(result.success).toBe(true)
|
||
// a.md: 2 处(# Hello / World hello again) + b.markdown: 1 处(HELLO UPPER)
|
||
expect(result.matches).toHaveLength(3)
|
||
expect(result.totalFiles).toBe(2)
|
||
})
|
||
|
||
it('should respect case-sensitive option', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: 'HELLO', caseSensitive: true })
|
||
expect(result.success).toBe(true)
|
||
expect(result.matches).toHaveLength(1)
|
||
expect(result.matches?.[0].filePath).toContain('b.markdown')
|
||
})
|
||
|
||
it('should support regex search', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: '^line', useRegex: true })
|
||
expect(result.success).toBe(true)
|
||
expect(result.matches).toHaveLength(1)
|
||
expect(result.matches?.[0].line).toBe(2)
|
||
})
|
||
|
||
it('should reject invalid regex with error', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: '([', useRegex: true })
|
||
expect(result.success).toBe(false)
|
||
expect(result.error).toBe('无效的正则表达式')
|
||
})
|
||
|
||
it('should skip node_modules and dotfiles', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: 'hidden' })
|
||
expect(result.success).toBe(true)
|
||
expect(result.matches).toHaveLength(0)
|
||
})
|
||
|
||
it('should reject empty query', async () => {
|
||
const result = await searchInDir({ dirPath: dir, query: '' })
|
||
expect(result.success).toBe(false)
|
||
})
|
||
|
||
it('should return error for invalid directory', async () => {
|
||
const result = await searchInDir({ dirPath: join(dir, 'nope'), query: 'x' })
|
||
// 目录不存在:walk 内部吞掉 readdir 错误,返回 0 结果(success: true)
|
||
expect(result.success).toBe(true)
|
||
expect(result.matches).toHaveLength(0)
|
||
})
|
||
})
|