import { test } from 'node:test'; import assert from 'node:assert/strict'; import { diffWords, DiffSegment } from './textDiff'; // Helper: reconstruct the old / new text from segments to prove correctness. const oldText = (segs: DiffSegment[]) => segs .filter((s) => s.type !== 'added') .map((s) => s.text) .join(''); const newText = (segs: DiffSegment[]) => segs .filter((s) => s.type !== 'removed') .map((s) => s.text) .join(''); test('identical text yields a single equal segment', () => { const out = diffWords('hello world', 'hello world'); assert.deepEqual(out, [{ type: 'equal', text: 'hello world' }]); }); test('pure insertion marks only the new words as added', () => { const out = diffWords('hello world', 'hello there world'); assert.equal(oldText(out), 'hello world'); assert.equal(newText(out), 'hello there world'); assert.deepEqual( out.filter((s) => s.type === 'added').map((s) => s.text.trim()), ['there'], ); assert.equal( out.some((s) => s.type === 'removed'), false, ); }); test('pure deletion marks only the dropped words as removed', () => { const out = diffWords('hello there world', 'hello world'); assert.equal( out.some((s) => s.type === 'added'), false, ); assert.deepEqual( out.filter((s) => s.type === 'removed').map((s) => s.text.trim()), ['there'], ); }); test('a word replacement is a removed run followed by an added run', () => { const out = diffWords('hello world', 'hi world'); // Reconstructs both sides. assert.equal(oldText(out), 'hello world'); assert.equal(newText(out), 'hi world'); const removed = out.filter((s) => s.type === 'removed').map((s) => s.text.trim()); const added = out.filter((s) => s.type === 'added').map((s) => s.text.trim()); assert.deepEqual(removed, ['hello']); assert.deepEqual(added, ['hi']); }); test('whitespace and newlines are preserved in reconstruction', () => { const a = ' line one\nline two '; const b = ' line one\nline three '; const out = diffWords(a, b); assert.equal(oldText(out), a); assert.equal(newText(out), b); }); test('empty <-> non-empty', () => { assert.deepEqual(diffWords('', 'new text'), [{ type: 'added', text: 'new text' }]); assert.deepEqual(diffWords('old text', ''), [{ type: 'removed', text: 'old text' }]); assert.deepEqual(diffWords('', ''), []); }); test('diffWords does not mutate its inputs', () => { const a = 'alpha beta'; const b = 'alpha gamma'; diffWords(a, b); assert.equal(a, 'alpha beta'); assert.equal(b, 'alpha gamma'); }); test('very large inputs fall back to a coarse whole-block diff', () => { const big = Array.from({ length: 5000 }, (_, i) => `w${i}`).join(' '); const out = diffWords(big, `${big} extra`); // Coarse fallback: one removed block + one added block (no per-word LCS). assert.deepEqual( out.map((s) => s.type), ['removed', 'added'], ); assert.equal(oldText(out), big); assert.equal(newText(out), `${big} extra`); }); test('word-level granularity (not character-level)', () => { const out = diffWords('cat', 'cats'); // "cat" and "cats" are different tokens → full removed + added, no partial. assert.deepEqual( out.map((s) => s.type), ['removed', 'added'], ); });