196 lines
6.9 KiB
JavaScript
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('<')
|
|
})
|
|
|
|
it('assigns sequential line numbers', () => {
|
|
const result = compareTextByChar('a\nb', 'a\nb')
|
|
expect(result.left.map(l => l.lineNumber)).toEqual([1, 2])
|
|
})
|
|
})
|
|
})
|