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('