Files
DiffLens/src/renderer/src/diff/report.test.ts
T

291 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import { computeDiff, type DiffRow } from './diffEngine'
import {
buildHtmlReport,
buildTxtReport,
buildMarkdownReport,
buildReport,
buildSegments,
plainSummary,
plainOptions,
REPORT_EXT
} from './report'
const ctx = { leftName: 'a.txt', rightName: 'b.txt', leftEncoding: 'UTF-8', rightEncoding: 'UTF-8' }
/** 便捷构造行集:u=unchanged, a=added, r=removed, m=modified */
function rowsOf(kinds: string[]): DiffRow[] {
let n = 0
return kinds.map((k) => {
const id = `t${n++}`
const changed = k !== 'u'
const left = { lineNo: k === 'a' ? null : n, text: k === 'a' ? null : `L${n}`, segs: null }
const right = { lineNo: k === 'r' ? null : n, text: k === 'r' ? null : `R${n}`, segs: null }
return {
id,
rowKind: (k === 'a' ? 'added' : k === 'r' ? 'removed' : k === 'm' ? 'modified' : 'unchanged') as DiffRow['rowKind'],
isChanged: changed,
left,
right
}
})
}
describe('buildSegments - 差异分块', () => {
it('空行集返回空段', () => {
expect(buildSegments([])).toEqual([])
})
it('无任何变更时单段全展示且无省略', () => {
const segs = buildSegments(rowsOf(['u', 'u', 'u', 'u']))
expect(segs).toEqual([{ start: 0, end: 3, changes: 0, skippedBefore: 0, skippedAfter: 0 }])
})
it('单处居中变更保留前后 3 行上下文,其余省略', () => {
const rows = rowsOf(['u', 'u', 'u', 'u', 'u', 'm', 'u', 'u', 'u', 'u', 'u'])
const segs = buildSegments(rows)
expect(segs).toEqual([{ start: 2, end: 8, changes: 1, skippedBefore: 2, skippedAfter: 2 }])
})
it('变更在开头/结尾时区间收敛到边界', () => {
const rows = rowsOf(['m', 'u', 'u', 'u', 'u', 'm'])
const segs = buildSegments(rows)
expect(segs).toEqual([{ start: 0, end: 5, changes: 2, skippedBefore: 0, skippedAfter: 0 }])
})
it('相近变更(间距不超过 2×context+1)合并为同段', () => {
const rows = rowsOf(['u', 'u', 'u', 'u', 'u', 'm', 'u', 'u', 'u', 'm', 'u', 'u', 'u', 'u'])
// 变更 5 与 9:区间 [2,8] 与 [6,12] 重叠 → 单段 [2,12]
const segs = buildSegments(rows)
expect(segs).toHaveLength(1)
expect(segs[0].start).toBe(2)
expect(segs[0].end).toBe(12)
expect(segs[0].changes).toBe(2)
})
it('远距变更分为两段,段间省略数正确', () => {
const kinds = Array.from({ length: 30 }, () => 'u')
kinds[2] = 'm'
kinds[20] = 'm'
const segs = buildSegments(rowsOf(kinds))
expect(segs).toHaveLength(2)
expect(segs[0]).toMatchObject({ start: 0, end: 5, changes: 1, skippedBefore: 0 })
expect(segs[1]).toMatchObject({ start: 17, end: 23, changes: 1, skippedBefore: 11, skippedAfter: 6 })
})
it('自定义 context=0 时仅展示变更行本身', () => {
const rows = rowsOf(['u', 'u', 'u', 'm', 'u', 'u'])
const segs = buildSegments(rows, 0)
expect(segs).toEqual([{ start: 3, end: 3, changes: 1, skippedBefore: 3, skippedAfter: 2 }])
})
})
describe('plainSummary - 人话总结', () => {
it('无差异时给出一致结论', () => {
expect(plainSummary({ changedLines: 0, inserted: 0, deleted: 0, modified: 0 })).toBe(
'两文件内容完全一致'
)
})
it('为 0 的项被省略', () => {
expect(plainSummary({ changedLines: 2, inserted: 2, deleted: 0, modified: 0 })).toBe(
'共 2 处不同:新增 2 行'
)
})
it('三类变更齐全时完整罗列', () => {
expect(plainSummary({ changedLines: 9, inserted: 4, deleted: 3, modified: 2 })).toBe(
'共 9 处不同:新增 4 行、删除 3 行、修改 2 行'
)
})
})
describe('buildHtmlReport', () => {
const { rows, summary } = computeDiff('line1\nline2', 'line1\nline3')
it('输出合法 HTML 文档并包含核心信息', () => {
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('<!doctype html>')
expect(html).toContain('DiffLens 差异报告')
expect(html).toContain('a.txt')
expect(html).toContain('b.txt')
})
it('Hero 区包含人话结论、文件卡与统计卡', () => {
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('共 1 处不同:修改 1 行')
expect(html).toContain('class="verdict"')
expect(html).toContain('file-card left')
expect(html).toContain('file-card right')
expect(html).toContain('stat ins')
expect(html).toContain('stat rate')
})
it('包含差异统计与类型高亮语义', () => {
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('modified')
expect(html).toContain('hunk-head')
})
it('内容完全一致时显示绿色一致结论', () => {
const { rows, summary } = computeDiff('same', 'same')
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('两文件内容完全一致')
expect(html).toContain('verdict ok')
})
it('多差异块时渲染目录锚点,单块时不渲染目录', () => {
const left = Array.from({ length: 20 }, (_, i) => `l${i}`).join('\n')
const right = left.replace('l2', 'X').replace('l16', 'Y')
const multi = computeDiff(left, right)
const html = buildHtmlReport(multi.rows, ctx, multi.summary)
expect(html).toContain('toc-list')
expect(html).toContain('href="#seg-2"')
const single = computeDiff('a', 'b')
const singleHtml = buildHtmlReport(single.rows, ctx, single.summary)
expect(singleHtml).not.toContain('href="#seg-')
})
it('大段相同内容以折叠行呈现且保留全量数据', () => {
const left = Array.from({ length: 40 }, (_, i) => `l${i}`).join('\n')
const right = left.replace('l20', 'X')
const { rows, summary } = computeDiff(left, right)
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('<details><summary>⋯ 相同内容')
// 折叠段内仍包含被省略行的数据(信息零丢失)
expect(html).toContain('l0')
expect(html).toContain('l39')
})
it('两侧均为空时给出空内容提示', () => {
const { rows, summary } = computeDiff('', '')
const html = buildHtmlReport(rows, ctx, summary)
expect(html).toContain('没有可对比的行')
})
})
describe('plainOptions - 比较选项描述', () => {
it('未传或空选项时说明为严格对比', () => {
expect(plainOptions(undefined)).toBe('无(严格逐字符对比)')
expect(plainOptions({})).toBe('无(严格逐字符对比)')
})
it('列出全部生效选项', () => {
expect(
plainOptions({ ignoreAllWhitespace: true, ignoreCase: true, ignoreBlankLines: true })
).toBe('忽略所有空白、忽略大小写、忽略空行')
expect(plainOptions({ trimWhitespace: true, ignoreCase: true })).toBe(
'忽略行首尾空白、忽略大小写'
)
})
it('忽略所有空白遮蔽行首尾空白(超集不重复列出)', () => {
expect(plainOptions({ ignoreAllWhitespace: true, trimWhitespace: true })).toBe('忽略所有空白')
})
it('字符级对比单独描述并遮蔽全部空白类选项', () => {
expect(
plainOptions({ charMode: true, trimWhitespace: true, ignoreAllWhitespace: true, ignoreBlankLines: true })
).toBe('字符级对比(忽略全部空白与换行)')
})
it('字符级对比与忽略大小写组合列出', () => {
expect(plainOptions({ charMode: true, ignoreCase: true })).toBe(
'字符级对比(忽略全部空白与换行)、忽略大小写'
)
})
})
describe('报告头部比较选项说明', () => {
const { rows, summary } = computeDiff('a b', 'ab', { ignoreAllWhitespace: true })
const optCtx = {
...ctx,
options: { ignoreAllWhitespace: true, ignoreCase: true }
}
it('纯文本头部包含比较选项行', () => {
const txt = buildTxtReport(rows, optCtx, summary)
expect(txt).toContain('比较选项:忽略所有空白、忽略大小写')
})
it('Markdown 头部包含比较选项行(引用块内)', () => {
const md = buildMarkdownReport(rows, optCtx, summary)
expect(md).toContain('> 比较选项:忽略所有空白、忽略大小写')
})
it('HTML Hero 区包含比较选项徽标', () => {
const html = buildHtmlReport(rows, optCtx, summary)
expect(html).toContain('hero-opts')
expect(html).toContain('比较选项:忽略所有空白、忽略大小写')
})
it('未传选项时头部说明为严格对比', () => {
const txt = buildTxtReport(rows, ctx, summary)
expect(txt).toContain('比较选项:无(严格逐字符对比)')
})
})
describe('buildTxtReport', () => {
it('输出带增删改标记与人话结论的行式文本', () => {
const { rows, summary } = computeDiff('a', 'a\nb')
const txt = buildTxtReport(rows, ctx, summary)
expect(txt).toContain('DiffLens')
expect(txt).toContain('+')
expect(txt).toContain('结论:共 1 处不同:新增 1 行')
})
it('行号按最大位数右对齐填充', () => {
const left = Array.from({ length: 12 }, (_, i) => `l${i}`).join('\n')
const right = left.replace('l1', 'changed')
const { rows, summary } = computeDiff(left, right)
const txt = buildTxtReport(rows, ctx, summary)
// 最大行号 12 → 宽度 2:行号 2 左补空格,行号 12 原样(修改行左右行号同样填充)
expect(txt).toContain('~ 2 | l1 ==> 2 | changed')
expect(txt).toContain(' 12 | l11')
})
it('行号位数一致时无额外填充', () => {
const { rows, summary } = computeDiff('a\nc', 'a\nb')
const txt = buildTxtReport(rows, ctx, summary)
expect(txt).toContain('~ 2 | c ==> 2 | b')
})
})
describe('buildMarkdownReport', () => {
it('输出表格化报告,标题独立成行不进引用块', () => {
const { rows, summary } = computeDiff('a', 'b')
const md = buildMarkdownReport(rows, ctx, summary)
expect(md.startsWith('# DiffLens 差异报告')).toBe(true)
expect(md).not.toContain('> # DiffLens 差异报告')
expect(md).toContain('| 类型 |')
expect(md).toContain('| 左行号 |')
})
it('引用块包含人话结论', () => {
const { rows, summary } = computeDiff('a', 'b')
const md = buildMarkdownReport(rows, ctx, summary)
expect(md).toContain('> 结论:共 1 处不同:修改 1 行')
})
it('管道符被转义以保持表格结构', () => {
const { rows, summary } = computeDiff('a|b', 'a')
const md = buildMarkdownReport(rows, ctx, summary)
expect(md).toContain('a\\|b')
})
})
describe('buildReport / REPORT_EXT', () => {
it('按格式分发', () => {
const { rows, summary } = computeDiff('x', 'y')
expect(buildReport(rows, ctx, summary, 'html')).toContain('<!doctype html>')
expect(buildReport(rows, ctx, summary, 'txt')).toContain('DiffLens')
expect(buildReport(rows, ctx, summary, 'md')).toContain('# DiffLens 差异报告')
})
it('扩展名映射正确', () => {
expect(REPORT_EXT.html).toBe('html')
expect(REPORT_EXT.txt).toBe('txt')
expect(REPORT_EXT.md).toBe('md')
})
})