291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
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')
|
||
})
|
||
}) |