Prevention work surfaced a real latent bug: findAndReplace looped forever (OOM) on any non-global regex with a match — `match` was only reassigned inside `if (regex.global)`, so a non-global regex never advanced. Fixed by treating a non-global regex as a single match (`match = regex.global ? regex.exec(text) : null`) and added a regression test. Latent in practice (all current callers pass global regexes), but a crash waiting to happen. New suites (tsx + node:test), verified empirically: - utils/findAndReplace (10, incl. the regression) - utils/AsyncSearch (9): normalize + matchQuery (the timer-based class is skipped — needs window.performance/setTimeout, unavailable in node) - utils/ASCIILexicalTable (10): orderKeys gap-filling + invariants Full suite now 103 tests, all passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { normalize, matchQuery } from './AsyncSearch';
|
|
|
|
test('normalize lowercases and strips whitespace by default', () => {
|
|
assert.equal(normalize('Hello World'), 'helloworld');
|
|
assert.equal(normalize(' A B '), 'ab');
|
|
assert.equal(normalize(''), '');
|
|
});
|
|
|
|
test('normalize strips all whitespace kinds by default', () => {
|
|
assert.equal(normalize('a\tb\nc\r d'), 'abcd');
|
|
});
|
|
|
|
test('normalize caseSensitive keeps original case', () => {
|
|
assert.equal(normalize('Hello World', { caseSensitive: true }), 'HelloWorld');
|
|
});
|
|
|
|
test('normalize ignoreWhitespace=false keeps whitespace', () => {
|
|
assert.equal(normalize('Hello World', { ignoreWhitespace: false }), 'hello world');
|
|
});
|
|
|
|
test('normalize default uses NFKC (compatibility) unicode normalization', () => {
|
|
// U+FB01 (fi ligature) decomposes to "fi" under NFKC but not NFC.
|
|
assert.equal(normalize('file'), 'file');
|
|
// With normalizeUnicode disabled (NFC), the ligature is preserved.
|
|
assert.equal(normalize('file', { normalizeUnicode: false }), 'file');
|
|
});
|
|
|
|
test('normalize does not strip diacritics', () => {
|
|
// normalize only does NFKC/NFC + lowercase; it does not remove accents.
|
|
assert.equal(normalize('Café'), 'café');
|
|
});
|
|
|
|
test('matchQuery default matches by prefix', () => {
|
|
assert.equal(matchQuery('hello world', 'hello'), true);
|
|
assert.equal(matchQuery('hello world', 'world'), false);
|
|
assert.equal(matchQuery('hello', ''), true);
|
|
assert.equal(matchQuery('', 'a'), false);
|
|
});
|
|
|
|
test('matchQuery contain option matches anywhere', () => {
|
|
assert.equal(matchQuery('hello world', 'world', { contain: true }), true);
|
|
assert.equal(matchQuery('hello world', 'lo wo', { contain: true }), true);
|
|
assert.equal(matchQuery('hello world', 'xyz', { contain: true }), false);
|
|
assert.equal(matchQuery('hello', '', { contain: true }), true);
|
|
});
|
|
|
|
test('matchQuery is case sensitive (does not normalize internally)', () => {
|
|
assert.equal(matchQuery('Hello', 'hello'), false);
|
|
assert.equal(matchQuery('Hello', 'Hello'), true);
|
|
});
|