Files
ToolBox/tests/unit/comparator/textDiff.test.js
T
renjue f1502ed08c
CI / docker (push) Successful in 11m42s
初始化 ToolBox 开发者工具箱。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 11:15:38 +08:00

196 lines
6.9 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
computeDiff,
shouldMergeAsModify,
compareTextByLine,
compareTextByChar,
} from '../../../src/utils/comparator/textDiff.js'
describe('comparator/textDiff', () => {
describe('shouldMergeAsModify', () => {
it('returns false for identical lines', () => {
expect(shouldMergeAsModify('same', 'same')).toBe(false)
})
it('merges JSON key-value lines with same key', () => {
expect(shouldMergeAsModify(' "name": "Alice"', ' "name": "Bob"')).toBe(true)
})
it('does not merge JSON key-value lines with different keys', () => {
expect(shouldMergeAsModify(' "name": "Alice"', ' "age": 30')).toBe(false)
})
it('merges structurally similar lines (>50% prefix)', () => {
expect(shouldMergeAsModify(' { "a": 1 }', ' { "a": 2 }')).toBe(true)
})
it('does not merge completely different lines', () => {
expect(shouldMergeAsModify('foo', 'bar')).toBe(false)
})
})
describe('computeDiff', () => {
it('finds full match path', () => {
const path = computeDiff(['a', 'b'], ['a', 'b'])
expect(path.some(p => p.x === 0 && p.y === 0)).toBe(true)
expect(path.some(p => p.x === 1 && p.y === 1)).toBe(true)
})
it('prefers leftmost match for duplicate chars in B (hello vs helloworld)', () => {
const path = computeDiff('hello'.split(''), 'helloworld'.split(''))
const oMatch = path.find(p => {
const charA = 'hello'[p.x]
const charB = 'helloworld'[p.y]
return charA === 'o' && charB === 'o'
})
expect(oMatch).toBeDefined()
expect(oMatch.y).toBe(4)
})
it('handles empty arrays', () => {
const path = computeDiff([], [])
expect(path).toEqual([{ x: 0, y: 0 }])
})
it('handles one empty array', () => {
const path = computeDiff(['a'], [])
expect(path[path.length - 1]).toEqual({ x: 1, y: 0 })
})
})
describe('compareTextByLine', () => {
it('marks identical multi-line text as all same', () => {
const result = compareTextByLine('line1\nline2\nline3', 'line1\nline2\nline3')
expect(result.left.every(l => l.type === 'same')).toBe(true)
expect(result.right.every(l => l.type === 'same')).toBe(true)
expect(result.stats).toEqual({ same: 3, insert: 0, delete: 0, modify: 0 })
})
it('handles empty strings (single empty line)', () => {
const result = compareTextByLine('', '')
expect(result.stats.same).toBe(1)
})
it('detects pure insertion on right side', () => {
const result = compareTextByLine('a', 'a\nb')
expect(result.stats.insert).toBeGreaterThan(0)
const insertLine = result.right.find(l => l.type === 'insert')
expect(insertLine?.content).toBe('b')
})
it('detects pure deletion on right side', () => {
const result = compareTextByLine('a\nb', 'a')
expect(result.stats.delete).toBeGreaterThan(0)
const deleteLine = result.left.find(l => l.type === 'delete')
expect(deleteLine?.content).toBe('b')
})
it('merges similar JSON lines as modify', () => {
const result = compareTextByLine(
' "name": "Alice"',
' "name": "Bob"'
)
expect(result.stats.modify).toBe(1)
expect(result.left[0].type).toBe('modify')
expect(result.right[0].type).toBe('modify')
})
it('shows delete+insert for different JSON keys', () => {
const result = compareTextByLine(
' "name": "Alice"',
' "age": 30'
)
expect(result.stats.delete).toBe(1)
expect(result.stats.insert).toBe(1)
expect(result.stats.modify).toBe(0)
})
it('assigns line numbers correctly for same lines', () => {
const result = compareTextByLine('a\nb', 'a\nb')
expect(result.left[0].lineNumber).toBe(1)
expect(result.left[1].lineNumber).toBe(2)
})
it('sets null lineNumber for placeholder lines on opposite side', () => {
const result = compareTextByLine('only-left', 'only-right')
const leftInsert = result.left.find(l => l.type === 'insert')
const rightDelete = result.right.find(l => l.type === 'delete')
if (leftInsert) expect(leftInsert.lineNumber).toBeNull()
if (rightDelete) expect(rightDelete.lineNumber).toBeNull()
})
it('handles completely different single lines', () => {
const result = compareTextByLine('aaa', 'bbb')
expect(result.stats.delete + result.stats.insert + result.stats.modify).toBeGreaterThan(0)
})
it('handles trailing newline difference consistently', () => {
const result = compareTextByLine('a\n', 'a')
expect(result.left.length).toBe(result.right.length)
})
})
describe('compareTextByChar', () => {
it('marks identical lines as same with char count in stats', () => {
const result = compareTextByChar('hello', 'hello')
expect(result.left[0].type).toBe('same')
expect(result.stats.same).toBe(5)
})
it('highlights character differences inline', () => {
const result = compareTextByChar('abc', 'adc')
expect(result.left[0].inlineHighlight).toBe(true)
expect(result.left[0].html).toContain('diff-highlight')
})
it('handles empty vs non-empty line', () => {
const result = compareTextByChar('', 'hello')
expect(result.stats.insert).toBeGreaterThan(0)
})
it('handles insertion at end (hello vs helloworld)', () => {
const result = compareTextByChar('hello', 'helloworld')
expect(result.stats.insert).toBeGreaterThan(0)
expect(result.left[0].html).toContain('hello')
})
it('handles deletion (helloworld vs hello)', () => {
const result = compareTextByChar('helloworld', 'hello')
expect(result.stats.delete).toBeGreaterThan(0)
})
it('handles modify at start (na vs aa)', () => {
const result = compareTextByChar('na', 'aa')
expect(result.stats.delete).toBeGreaterThan(0)
expect(result.stats.insert).toBeGreaterThan(0)
expect(result.left[0].inlineHighlight).toBe(true)
})
it('aligns lines by index across multiline text', () => {
const result = compareTextByChar('a\nb', 'a\nc')
expect(result.left).toHaveLength(2)
expect(result.right).toHaveLength(2)
expect(result.left[0].type).toBe('same')
expect(result.left[1].type).toBe('modify')
})
it('handles extra lines on one side', () => {
const result = compareTextByChar('a', 'a\nb')
expect(result.left.length).toBe(2)
expect(result.left[1].content).toBe('')
expect(result.right[1].content).toBe('b')
})
it('escapes HTML in diff output', () => {
const result = compareTextByChar('<script>', '<script>')
expect(result.left[0].html).not.toContain('<script>')
expect(result.left[0].html).toContain('&lt;')
})
it('assigns sequential line numbers', () => {
const result = compareTextByChar('a\nb', 'a\nb')
expect(result.left.map(l => l.lineNumber)).toEqual([1, 2])
})
})
})