feat: 0.9.0 忽略规则、增量缓存跨会话持久化、快照基线、字符级预算自适应与性能基准E2E - 忽略规则(gitignore简化子集:目录整树剪枝不枚举不计数、扩展名通配与名字匹配、随偏好持久化)、扫描增量缓存落盘userData跨会话自动注入(LRU 8对、防御性解析、渲染端零改动)、快照基线(保存/加载JSON对照六类状态变迁统计与条目标注、对照随重扫刷新、报告附快照口径)、字符级变更组预算随输入规模自适应(下限与固定值一致小输入零变化、大输入按总组流半数放宽上限100万封顶)、大文件性能基准E2E(5万行<15s与300KB GBK<10s耗时断言)+新功能E2E、文档与版本号同步

This commit is contained in:
2026-08-18 22:58:00 +08:00
parent fa13160848
commit 35fa81df9d
26 changed files with 2525 additions and 550 deletions
+192
View File
@@ -388,6 +388,141 @@ test.describe('文件夹对比(主进程真实扫描链路)', () => {
await removeTmpDir(root)
}
})
test('忽略规则:目录整树剪枝不参与对比(应用后重扫)', async () => {
const root = await makeTmpDir('difflens-e2e-ignore-')
try {
const leftDir = join(root, 'left')
const rightDir = join(root, 'right')
const { mkdir, writeFile: wf } = await import('fs/promises')
await mkdir(join(leftDir, 'node_modules', 'pkg'), { recursive: true })
await mkdir(join(rightDir, 'node_modules', 'pkg'), { recursive: true })
await wf(join(leftDir, 'keep.txt'), 'identical')
await wf(join(rightDir, 'keep.txt'), 'identical')
await wf(join(leftDir, 'node_modules', 'pkg', 'x.js'), 'left')
await wf(join(rightDir, 'node_modules', 'pkg', 'x.js'), 'right')
const { app, window } = await launchApp()
await stubOpenDialogSeq(app, [[leftDir], [rightDir]])
await window.getByRole('button', { name: '对比文件夹' }).click()
// 无规则:node_modules 内差异文件参与对比
await expect(window.getByText('不同 1')).toBeVisible()
await expect(window.getByText('x.js')).toBeVisible()
// 应用忽略规则:node_modules/ 整树剪枝
await window.getByRole('button', { name: '忽略规则' }).click()
await window.getByPlaceholder(/每行一条/).fill('node_modules/')
await window.getByText('应用并重扫').click()
// 重扫完成:无差异(keep.txt 一致,node_modules 被忽略)
await expect(window.getByText('不同 0')).toBeVisible()
await expect(window.getByText('没有差异文件(两侧内容全部一致)')).toBeVisible()
await expect(window.getByText('x.js')).toHaveCount(0)
await app.close()
} finally {
await removeTmpDir(root)
}
})
test('增量缓存跨会话持久化:落盘文件存在且重启后重扫正确', async () => {
const root = await makeTmpDir('difflens-e2e-cachestore-')
try {
const leftDir = join(root, 'left')
const rightDir = join(root, 'right')
const { mkdir, writeFile: wf } = await import('fs/promises')
await mkdir(leftDir, { recursive: true })
await mkdir(rightDir, { recursive: true })
await wf(join(leftDir, 'same.txt'), 'identical')
await wf(join(rightDir, 'same.txt'), 'identical')
await wf(join(leftDir, 'diff.txt'), 'left v1')
await wf(join(rightDir, 'diff.txt'), 'right v1')
// 第一实例:扫描后缓存异步落盘 userData/scan-cache.json
const first = await launchApp()
await stubOpenDialogSeq(first.app, [[leftDir], [rightDir]])
await first.window.getByRole('button', { name: '对比文件夹' }).click()
await expect(first.window.getByText('相同 1')).toBeVisible()
const cachePath = await first.app.evaluate(({ app }) => app.getPath('userData')).then(
(userData) => join(userData, 'scan-cache.json')
)
await first.app.close()
// 落盘文件存在且含本目录对的缓存条目(等待异步写盘完成)
let raw = ''
for (let i = 0; i < 20; i++) {
raw = await readFile(cachePath, 'utf-8').catch(() => '')
if (raw.includes('same.txt')) break
await new Promise((r) => setTimeout(r, 200))
}
expect(raw).toContain('same.txt')
expect(raw).toContain('diff.txt')
// 结构为合法存储(版本 + 目录对键)
const stored = JSON.parse(raw)
expect(stored.version).toBe(1)
expect(Object.keys(stored.pairs).length).toBeGreaterThanOrEqual(1)
// 第二实例(同 userData,渲染端无内存缓存):主进程注入盘上缓存,重扫结果正确
const second = await launchApp({ fresh: false })
await stubOpenDialogSeq(second.app, [[leftDir], [rightDir]])
await second.window.getByRole('button', { name: '对比文件夹' }).click()
await expect(second.window.getByText('相同 1')).toBeVisible()
await expect(second.window.getByText('不同 1')).toBeVisible()
await second.app.close()
} finally {
await removeTmpDir(root)
}
})
test('快照基线:保存后修复差异并新增文件,加载快照对照变迁', async () => {
const root = await makeTmpDir('difflens-e2e-snapshot-')
try {
const leftDir = join(root, 'left')
const rightDir = join(root, 'right')
const { mkdir, writeFile: wf } = await import('fs/promises')
await mkdir(leftDir, { recursive: true })
await mkdir(rightDir, { recursive: true })
await wf(join(leftDir, 'same.txt'), 'identical')
await wf(join(rightDir, 'same.txt'), 'identical')
await wf(join(leftDir, 'diff.txt'), 'left v1')
await wf(join(rightDir, 'diff.txt'), 'right v1')
const { app, window } = await launchApp()
await stubOpenDialogSeq(app, [[leftDir], [rightDir]])
await window.getByRole('button', { name: '对比文件夹' }).click()
await expect(window.getByText('相同 1')).toBeVisible()
await expect(window.getByText('不同 1')).toBeVisible()
// 保存快照(真实写盘)
const snapPath = join(root, 'baseline.json')
await stubSaveDialog(app, snapPath)
await window.getByRole('button', { name: '保存快照' }).click()
await expect(window.getByText(/快照已保存/)).toBeVisible()
const snapRaw = await readFile(snapPath, 'utf-8')
expect(snapRaw).toContain('"version": 1')
expect(snapRaw).toContain('diff.txt')
// 修复 diff.txt(两侧一致)+ 新增 new.txt → 重扫
await wf(join(rightDir, 'diff.txt'), 'left v1')
await wf(join(leftDir, 'new.txt'), 'n')
await wf(join(rightDir, 'new.txt'), 'n')
await window.getByRole('button', { name: '重新扫描' }).click()
await expect(window.getByText('相同 3')).toBeVisible()
// 加载快照对照:diff.txt 已修复、new.txt 新文件
await stubOpenDialogSeq(app, [[snapPath]])
await window.getByRole('button', { name: '加载快照' }).click()
await expect(window.getByText(/已加载快照/)).toBeVisible()
await expect(window.getByText('已修复 1')).toBeVisible()
await expect(window.getByText('新文件 1')).toBeVisible()
// 对照条目行(平铺)与变迁标注可见;退出对照恢复
await expect(window.getByText('diff.txt')).toBeVisible()
await expect(window.getByText('new.txt')).toBeVisible()
await window.getByRole('button', { name: '退出对照' }).click()
await expect(window.getByText('相同 3')).toBeVisible()
await app.close()
} finally {
await removeTmpDir(root)
}
})
})
test.describe('拖拽导入(真实 Chromium DataTransfer 链路)', () => {
@@ -462,3 +597,60 @@ test.describe('偏好持久化(跨实例重启)', () => {
await second.app.close()
})
})
test.describe('大文件性能基准(耗时上限断言)', () => {
// 口径说明:万级行 diff 在 worker 后台实测为秒级;上限按慢机约 4 倍余量放宽
// 防 CI flaky(用例超时 60s 兜底)。断言失败即性能回归,需排查引擎/IO 退化。
test('5 万行多差异文件:后台计算完成在耗时上限内', async () => {
const dir = await makeTmpDir('difflens-e2e-perf-diff-')
try {
const n = 50000
const pad = (i: number): string => String(i).padStart(5, '0')
const left = Array.from({ length: n }, (_, i) => `L${pad(i)}-${'x'.repeat(30)}`)
const right = [...left]
// 20 处分散修改(每 2500 行一处)
for (let i = 0; i < n; i += 2500) right[i] = `R${pad(i)}-${'y'.repeat(30)}`
const lp = await writeTextFixture(dir, 'big-left.txt', left.join('\n'))
const rp = await writeTextFixture(dir, 'big-right.txt', right.join('\n'))
const { app, window } = await launchApp()
await stubOpenDialogSeq(app, [[lp], [rp]])
await window.getByRole('button', { name: '打开左侧' }).click()
await window.getByText('L00000-').first().waitFor()
// 计时:打开右侧 → worker 重路径计算完成(有差异徽章出现)
const t0 = Date.now()
await window.getByRole('button', { name: '打开右侧' }).click()
await window.getByText('有差异').first().waitFor()
const elapsed = Date.now() - t0
expect(elapsed).toBeLessThan(15000)
// 完整性:双侧 5 万行均已加载
await expect(window.locator('.status-item').filter({ hasText: '50000 行' })).toHaveCount(2)
await app.close()
} finally {
await removeTmpDir(dir)
}
})
test('大体积 GBK 文件:worker 后台解码渲染在耗时上限内', async () => {
const dir = await makeTmpDir('difflens-e2e-perf-gbk-')
try {
// 约 300KB(≥ 解码 worker 快路径阈值 256KB),15000 行
const bigLines = Array.from({ length: 15000 }, (_, i) => `中文大文件行${i}数据`).join('\n')
const bigPath = await writeTextFixture(dir, 'big-gbk.txt', bigLines, 'gbk')
const { app, window } = await launchApp()
await stubOpenDialogSeq(app, [[bigPath]])
const t0 = Date.now()
await window.getByRole('button', { name: '打开左侧' }).click()
// 完整渲染以状态栏总行数为准(末行在虚拟滚动视口外)
await expect(
window.locator('.status-item').filter({ hasText: '15000 行' })
).toBeVisible()
const elapsed = Date.now() - t0
expect(elapsed).toBeLessThan(10000)
await expect(window.getByText('中文大文件行0数据')).toBeVisible()
await app.close()
} finally {
await removeTmpDir(dir)
}
})
})