import { test } from 'node:test'; import assert from 'node:assert/strict'; import { findAndReplace } from './findAndReplace'; // Helpers that record exactly what the callbacks receive. const tagPart = (text: string, pushIndex: number) => ({ part: text, at: pushIndex }); const tagMatch = (match: RegExpExecArray | RegExpMatchArray, pushIndex: number) => ({ match: match[0], at: pushIndex, }); test('findAndReplace interleaves converted parts and replacements (global regex)', () => { const result = findAndReplace('a1b2c', /\d/g, tagMatch, tagPart); assert.deepEqual(result, [ { part: 'a', at: 0 }, { match: '1', at: 1 }, { part: 'b', at: 2 }, { match: '2', at: 3 }, { part: 'c', at: 4 }, ]); }); test('findAndReplace handles a NON-global regex as a single match (regression: no infinite loop)', () => { // Before the fix, a non-global regex with a match looped forever (match was // only reassigned inside `if (regex.global)`), OOM-crashing the process. const result = findAndReplace('a1b2c', /\d/, tagMatch, tagPart); assert.deepEqual(result, [ { part: 'a', at: 0 }, { match: '1', at: 1 }, // remainder after the first (only) match is a single converted part { part: 'b2c', at: 2 }, ]); }); test('findAndReplace pushIndex reflects result.length at push time', () => { // The indices above already assert this; here we double-check the trailing part. const result = findAndReplace('x9', /\d/g, tagMatch, tagPart); // 'x' at 0, '9' at 1, '' at 2 assert.equal(result[result.length - 1].at, 2); }); test('findAndReplace with no match returns the whole text as a single converted part', () => { const result = findAndReplace('hello', /\d/g, tagMatch, tagPart); assert.deepEqual(result, [{ part: 'hello', at: 0 }]); }); test('findAndReplace on empty input returns a single empty converted part', () => { const result = findAndReplace('', /\d/g, tagMatch, tagPart); assert.deepEqual(result, [{ part: '', at: 0 }]); }); test('findAndReplace emits empty parts between adjacent matches and at edges', () => { const result = findAndReplace('12', /\d/g, tagMatch, tagPart); assert.deepEqual(result, [ { part: '', at: 0 }, { match: '1', at: 1 }, { part: '', at: 2 }, { match: '2', at: 3 }, { part: '', at: 4 }, ]); }); test('findAndReplace with a leading match emits an empty leading part', () => { const result = findAndReplace('1abc', /\d/g, tagMatch, tagPart); assert.deepEqual(result, [ { part: '', at: 0 }, { match: '1', at: 1 }, { part: 'abc', at: 2 }, ]); }); // NOTE: A non-global regex is intentionally NOT tested here. With `regex.global` // false, the source's `while (match !== null)` loop never reassigns `match` // (reassignment is guarded by `if (regex.global)`), so it loops forever on any // matching input. See the report. All real callers pass global regexes. test('findAndReplace supports multi-character matches', () => { const result = findAndReplace('foo<>baz', /<<|>>/g, tagMatch, tagPart); assert.deepEqual(result, [ { part: 'foo', at: 0 }, { match: '<<', at: 1 }, { part: 'bar', at: 2 }, { match: '>>', at: 3 }, { part: 'baz', at: 4 }, ]); }); test('findAndReplace can build a transformed string', () => { const out = findAndReplace( 'cat and dog', /cat|dog/g, (m) => (m[0] === 'cat' ? 'CAT' : 'DOG'), (text) => text, ).join(''); assert.equal(out, 'CAT and DOG'); }); test('findAndReplace passes the full match object to replace', () => { const captured: Array = []; findAndReplace( 'name=value', /(\w+)=(\w+)/g, (m) => { captured.push(m[1], m[2]); return null; }, () => null, ); assert.deepEqual(captured, ['name', 'value']); });