Files
lexgo/spikes/selection/range.test.mjs

84 lines
4.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { tokenize, normalizeRange, pointSelection, adjustRange, occurrences, resolveOverlaps } from './range.mjs';
test('partition preserves original Unicode, whitespace and punctuation exactly', () => {
const text = 'Hi, cafe\u0301!\n🙂 👩‍🚀 well-known 中文';
const tokens = tokenize(text);
assert.equal(tokens.map(t => t.text).join(''), text);
let end = 0;
for (const t of tokens) {
assert.equal(t.start, end);
assert.equal(text.slice(t.start, t.end), t.text);
end = t.end;
}
assert.deepEqual(tokens.filter(t => t.word).map(t => t.text), ['Hi', 'cafe\u0301', '🙂', '👩‍🚀', 'well', 'known', '中文']);
});
test('reverse partial selection aligns words and preserves interior punctuation/newlines', () => {
const text = ' bright,\nsmall step! ';
const tokens = tokenize(text);
const range = normalizeRange(text, tokens, 18, 4);
assert.equal(range?.text, 'bright,\nsmall step');
assert.equal(range.start, 2);
assert.equal(range.end, 20);
assert.deepEqual(normalizeRange(text, tokens, 0, text.length), range);
});
test('collapsed, whitespace, punctuation and invalid bounds do not select neighbors', () => {
const text = 'a, b';
const tokens = tokenize(text);
for (const [a, b] of [[1, 1], [1, 3], [-1, 2], [0, 5], [NaN, 3], [0.5, 3]]) {
assert.equal(normalizeRange(text, tokens, a, b), null);
}
});
test('selection cannot cut a surrogate pair, ZWJ or combining grapheme', () => {
const text = '🙂 e\u0301 👩‍🚀';
const tokens = tokenize(text);
const first = normalizeRange(text, tokens, 1, 2);
assert.equal(first?.text, '🙂');
const accent = normalizeRange(text, tokens, 4, 5);
assert.equal(accent?.text, 'e\u0301');
const astronaut = normalizeRange(text, tokens, 8, 9);
assert.equal(astronaut?.text, '👩‍🚀');
assert.deepEqual([astronaut.start_cp, astronaut.end_cp, astronaut.start_utf8, astronaut.end_utf8, astronaut.start_utf16, astronaut.end_utf16], [5, 8, 9, 20, 6, 11]);
});
test('point selection and boundary adjustment skip separators and never invert', () => {
const text = 'one, two\nthree';
const tokens = tokenize(text);
const middle = tokens.findIndex(t => t.text === 'two');
const one = pointSelection(text, tokens, middle);
assert.equal(one?.text, 'two');
assert.equal(pointSelection(text, tokens, middle - 1), null);
const left = adjustRange(text, tokens, one, 'start', -1);
assert.equal(left?.text, 'one, two');
const all = adjustRange(text, tokens, left, 'end', 1);
assert.equal(all?.text, text);
assert.equal(adjustRange(text, tokens, all, 'start', 1)?.text, 'two\nthree');
assert.equal(adjustRange(text, tokens, all, 'end', -1)?.text, 'one, two');
assert.deepEqual(adjustRange(text, tokens, one, 'start', 1), one);
assert.deepEqual(adjustRange(text, tokens, one, 'end', -1), one);
assert.deepEqual(adjustRange(text, tokens, all, 'start', -1), all);
assert.deepEqual(adjustRange(text, tokens, all, 'end', 1), all);
});
test('occurrences preserve distinct offsets, case and exact combining form', () => {
const text = 'a small step; a small step. A small step. steps e\u0301 é';
assert.deepEqual(occurrences(text, 'a small step').map(r => [r.start, r.end]), [[0, 12], [14, 26]]);
assert.equal(occurrences(text, 'step').length, 3);
assert.equal(occurrences(text, 'e\u0301').length, 1);
assert.equal(occurrences(text, 'é').length, 1);
assert.equal(occurrences(text, 'small st').length, 0);
assert.deepEqual(occurrences(text, ''), []);
});
test('overlapping occurrences remain available; display picks leftmost longest without mutation', () => {
assert.deepEqual(occurrences('a a a', 'a a').map(r => r.start), [0, 2]);
const ranges = [{ start: 2, end: 9, id: 'later' }, { start: 0, end: 3, id: 'short' }, { start: 0, end: 5, id: 'long' }, { start: 5, end: 7, id: 'next' }];
const before = structuredClone(ranges);
assert.deepEqual(resolveOverlaps(ranges).map(r => r.id), ['long', 'next']);
assert.deepEqual(ranges, before);
});