@sideid/id-profanity-filter 1.11.12 → 1.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ import { IDProfanityFilter } from '../src/index';
2
+
3
+ describe('IDProfanityFilter', () => {
4
+ let filter: IDProfanityFilter;
5
+
6
+ beforeEach(() => {
7
+ filter = new IDProfanityFilter();
8
+ });
9
+
10
+ describe('basic functionality', () => {
11
+ test('should detect profanity', () => {
12
+ expect(filter.isProfane('Ini kalimat tanpa kata kotor')).toBe(false);
13
+ expect(filter.isProfane('Dasar anjing kamu!')).toBe(true);
14
+ });
15
+
16
+ test('should filter profanity', () => {
17
+ const result = filter.filter('Dasar anjing kamu!');
18
+ expect(result.filtered).not.toEqual('Dasar anjing kamu!');
19
+ expect(result.filtered).toContain('*****');
20
+ expect(result.censored).toBe(1);
21
+ });
22
+
23
+ test('should analyze profanity', () => {
24
+ const result = filter.analyze('Kamu ini babi dan anjing!');
25
+ expect(result.hasProfanity).toBe(true);
26
+ expect(result.matches.length).toBe(2);
27
+ expect(result.severityScore).toBeGreaterThan(0);
28
+ });
29
+ });
30
+
31
+ describe('options configuration', () => {
32
+ test('should apply options when filtering', () => {
33
+ filter.setOptions({ replaceWith: '#' });
34
+ const result = filter.filter('Dasar anjing kamu!');
35
+ expect(result.filtered).toContain('#####');
36
+ expect(result.filtered).not.toContain('*****');
37
+ });
38
+
39
+ test('should apply keep first and last option', () => {
40
+ filter.setOptions({ replaceWith: '*', fullWordCensor: false, keepFirstAndLast: true });
41
+ const result = filter.filter('Dasar anjing kamu!');
42
+ expect(result.filtered).toContain('Dasar a****g kamu!');
43
+ });
44
+
45
+ test('should apply random grawlix option', () => {
46
+ filter.setOptions({ useRandomGrawlix: true });
47
+ const result = filter.filter('Dasar anjing kamu!');
48
+ // Should replace with random characters from the grawlix set
49
+ expect(result.filtered).not.toContain('anjing');
50
+ expect(result.filtered).not.toEqual('Dasar anjing kamu!');
51
+ });
52
+ });
53
+
54
+ describe('preset functionality', () => {
55
+ test('should apply presets correctly', () => {
56
+ filter.usePreset('strict');
57
+ expect(filter.filter('Dasar anjing!').censored).toBe(1);
58
+
59
+ filter.usePreset('childSafe');
60
+ const result = filter.filter('Kamu ini babi dan anjing!');
61
+ expect(result.censored).toBe(2);
62
+ });
63
+ });
64
+
65
+ describe('whitelist functionality', () => {
66
+ test('should respect whitelist', () => {
67
+ filter.addToWhitelist('anjing');
68
+ expect(filter.isProfane('anjing')).toBe(false);
69
+
70
+ filter.removeFromWhitelist('anjing');
71
+ expect(filter.isProfane('anjing')).toBe(true);
72
+ });
73
+ });
74
+
75
+ describe('advanced detection', () => {
76
+ test('should detect leet speak variations', () => {
77
+ filter.setOptions({ detectLeetSpeak: true });
78
+ expect(filter.isProfane('4nj1ng')).toBe(true);
79
+ });
80
+
81
+ test('should detect split words', () => {
82
+ filter.enableSplitWordDetection();
83
+ expect(filter.isProfane('a-n-j-i-n-g')).toBe(true);
84
+ });
85
+
86
+ test('should detect similar words with levenshtein', () => {
87
+ filter.enableLevenshteinDetection(0.8, 2);
88
+ expect(filter.isProfane('anjiing')).toBe(true); // Extra 'i'
89
+ });
90
+ });
91
+
92
+ describe('batch analysis', () => {
93
+ test('should analyze multiple strings correctly', () => {
94
+ const texts = [
95
+ 'Ini kalimat bersih tanpa kata kotor',
96
+ 'Dasar anjing kamu!',
97
+ 'Saya sangat suka kucing dan anjing peliharaan',
98
+ ];
99
+
100
+ const result = filter.batchAnalyze(texts);
101
+ expect(result.totalTexts).toBe(3);
102
+ expect(result.profaneTexts).toBe(2);
103
+ expect(result.cleanTexts).toBe(1);
104
+ });
105
+ });
106
+
107
+ describe('sentence analysis', () => {
108
+ test('should analyze by sentence correctly', () => {
109
+ const text =
110
+ 'Kalimat pertama bersih. Kalimat kedua anjing kotor. Kalimat ketiga juga bersih.';
111
+ const results = filter.analyzeBySentence(text);
112
+
113
+ expect(results.length).toBe(3);
114
+ expect(results[0].hasProfanity).toBe(false);
115
+ expect(results[1].hasProfanity).toBe(true);
116
+ expect(results[2].hasProfanity).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe('context analysis', () => {
121
+ test('should provide context around profanity', () => {
122
+ const text = 'Saya sangat kesal dengan perilaku anjing tersebut karena sangat mengganggu';
123
+ const results = filter.analyzeWithContext(text, 3);
124
+
125
+ expect(results.length).toBe(1);
126
+ expect(results[0].word).toBe('anjing');
127
+ expect(results[0].context).toContain('kesal dengan perilaku anjing tersebut karena');
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,106 @@
1
+ import { AhoCorasick } from '../../src/utils/ahoCorasick';
2
+
3
+ describe('AhoCorasick', () => {
4
+ let ac: AhoCorasick;
5
+
6
+ beforeEach(() => {
7
+ ac = new AhoCorasick();
8
+ });
9
+
10
+ describe('basic functionality', () => {
11
+ test('should build trie and search patterns', () => {
12
+ ac.addPattern('anjing');
13
+ ac.addPattern('babi');
14
+ ac.build();
15
+
16
+ const matches = ac.search('Dasar anjing dan babi!');
17
+
18
+ expect(matches.size).toBe(2);
19
+ expect(matches.get('anjing')).toBe(1);
20
+ expect(matches.get('babi')).toBe(1);
21
+ });
22
+
23
+ test('should return unique matches', () => {
24
+ ac.addPattern('anjing');
25
+ ac.addPattern('babi');
26
+ ac.build();
27
+
28
+ const matches = ac.searchUnique('Dasar anjing dan babi dan anjing!');
29
+
30
+ expect(matches.size).toBe(2);
31
+ expect(matches.has('anjing')).toBe(true);
32
+ expect(matches.has('babi')).toBe(true);
33
+ });
34
+
35
+ test('should detect presence of patterns', () => {
36
+ ac.addPattern('anjing');
37
+ ac.addPattern('babi');
38
+ ac.build();
39
+
40
+ expect(ac.containsAny('Dasar anjing!')).toBe(true);
41
+ expect(ac.containsAny('Dasar kucing!')).toBe(false);
42
+ });
43
+
44
+ test('should count multiple occurrences', () => {
45
+ ac.addPattern('anjing');
46
+ ac.build();
47
+
48
+ const matches = ac.search('anjing dan anjing lagi anjing');
49
+
50
+ expect(matches.get('anjing')).toBe(3);
51
+ });
52
+ });
53
+
54
+ describe('edge cases', () => {
55
+ test('should handle empty search text', () => {
56
+ ac.addPattern('anjing');
57
+ ac.build();
58
+
59
+ const matches = ac.search('');
60
+ expect(matches.size).toBe(0);
61
+ });
62
+
63
+ test('should handle overlapping patterns', () => {
64
+ ac.addPattern('an');
65
+ ac.addPattern('anjing');
66
+ ac.build();
67
+
68
+ const matches = ac.search('anjing');
69
+
70
+ expect(matches.size).toBe(2);
71
+ expect(matches.has('an')).toBe(true);
72
+ expect(matches.has('anjing')).toBe(true);
73
+ });
74
+
75
+ test('should throw error when trying to add pattern after build', () => {
76
+ ac.build();
77
+ expect(() => ac.addPattern('test')).toThrow();
78
+ });
79
+ });
80
+
81
+ describe('failure function', () => {
82
+ test('should handle prefix matches', () => {
83
+ ac.addPattern('anjing');
84
+ ac.addPattern('ing');
85
+ ac.build();
86
+
87
+ const matches = ac.search('anjing');
88
+
89
+ expect(matches.size).toBe(2);
90
+ expect(matches.has('anjing')).toBe(true);
91
+ expect(matches.has('ing')).toBe(true);
92
+ });
93
+
94
+ test('should handle suffix matches', () => {
95
+ ac.addPattern('an');
96
+ ac.addPattern('anjing');
97
+ ac.build();
98
+
99
+ const matches = ac.search('anjing');
100
+
101
+ expect(matches.size).toBe(2);
102
+ expect(matches.has('an')).toBe(true);
103
+ expect(matches.has('anjing')).toBe(true);
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,111 @@
1
+ import {
2
+ createWordRegex,
3
+ addLeetSpeakVariations,
4
+ addSplitVariations,
5
+ createEvasionRegex,
6
+ addIndonesianVariations,
7
+ createIndonesianVariationRegex,
8
+ createContextRegex,
9
+ createWordFormRegex,
10
+ } from '../../src/utils/regexUtils';
11
+
12
+ describe('Regex Utils', () => {
13
+ describe('createWordRegex function', () => {
14
+ test('should create basic word regex', () => {
15
+ const regex = createWordRegex('test');
16
+ expect(regex.test('test')).toBe(true);
17
+ expect(regex.test('testing')).toBe(false);
18
+ expect(regex.test('a test')).toBe(true);
19
+ });
20
+
21
+ test('should handle case sensitivity', () => {
22
+ const regex = createWordRegex('test', { caseSensitive: true });
23
+ expect(regex.test('test')).toBe(true);
24
+ expect(regex.test('TEST')).toBe(false);
25
+ });
26
+
27
+ test('should handle leetspeak option', () => {
28
+ const regex = createWordRegex('test', { leetSpeak: true });
29
+ expect(regex.test('t3st')).toBe(true);
30
+ });
31
+
32
+ test('should handle Indonesian variation option', () => {
33
+ const regex = createWordRegex('kafir', { indonesianVariation: true });
34
+ expect(regex.test('kapir')).toBe(true);
35
+ });
36
+
37
+ test('should not use word boundary when wholeWord is false', () => {
38
+ const regex = createWordRegex('test', { wholeWord: false });
39
+ expect(regex.test('testing')).toBe(true);
40
+ });
41
+ });
42
+
43
+ describe('addLeetSpeakVariations function', () => {
44
+ test('should add leetspeak variations', () => {
45
+ const pattern = addLeetSpeakVariations('test');
46
+ const regex = new RegExp(pattern);
47
+
48
+ expect(regex.test('t3st')).toBe(true);
49
+ expect(regex.test('te5t')).toBe(true);
50
+ expect(regex.test('t35t')).toBe(true);
51
+ });
52
+ });
53
+
54
+ describe('addSplitVariations function', () => {
55
+ test('should add split variations', () => {
56
+ const pattern = addSplitVariations('test');
57
+ const regex = new RegExp(pattern);
58
+
59
+ expect(regex.test('t-e-s-t')).toBe(true);
60
+ expect(regex.test('t e s t')).toBe(true);
61
+ expect(regex.test('t.e.s.t')).toBe(true);
62
+ });
63
+ });
64
+
65
+ // describe('createEvasionRegex function', () => {
66
+ // test('should create regex to detect evasion attempts', () => {
67
+ // const regex = createEvasionRegex('test');
68
+
69
+ // expect(regex.test('t-e-s-t')).toBe(true);
70
+ // expect(regex.test('t e s t')).toBe(true);
71
+ // expect(regex.test('t.e.s.t')).toBe(true);
72
+ // expect(regex.test('t*e*s*t')).toBe(true);
73
+ // });
74
+ // });
75
+
76
+ describe('addIndonesianVariations function', () => {
77
+ test('should add Indonesian spelling variations', () => {
78
+ const pattern = addIndonesianVariations('cacing');
79
+ const regex = new RegExp(pattern);
80
+
81
+ expect(regex.test('kacing')).toBe(true); // c/k variation
82
+ });
83
+ });
84
+
85
+ describe('createContextRegex function', () => {
86
+ test('should create regex that captures context', () => {
87
+ const regex = createContextRegex('test', 2);
88
+ const text = 'This is a test of the context';
89
+ const match = regex.exec(text);
90
+
91
+ expect(match).not.toBeNull();
92
+ if (match) {
93
+ expect(match[1]).toBe('is a '); // Before context
94
+ expect(match[2]).toBe('test'); // The word
95
+ expect(match[3]).toBe(' of the'); // After context
96
+ }
97
+ });
98
+ });
99
+
100
+ // describe('createWordFormRegex function', () => {
101
+ // test('should create regex for different word forms', () => {
102
+ // const regex = createWordFormRegex('ajar');
103
+
104
+ // expect(regex.test('ajar')).toBe(true);
105
+ // expect(regex.test('belajar')).toBe(true);
106
+ // expect(regex.test('mengajar')).toBe(true);
107
+ // expect(regex.test('pengajaran')).toBe(true);
108
+ // expect(regex.test('ajari')).toBe(true);
109
+ // });
110
+ // });
111
+ });
@@ -0,0 +1,139 @@
1
+ import {
2
+ levenshteinDistance,
3
+ stringSimilarity,
4
+ findMostSimilar,
5
+ findMostSimilarWithLevenshtein,
6
+ isPossibleProfanityVariation,
7
+ findPossibleProfanityBySimiliarity,
8
+ findProfanityByLevenshteinDistance,
9
+ } from '../../src/utils/similarityUtils';
10
+
11
+ describe('Similarity Utils', () => {
12
+ describe('levenshteinDistance function', () => {
13
+ test('should calculate correct distance', () => {
14
+ expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
15
+ expect(levenshteinDistance('test', 'test')).toBe(0);
16
+ expect(levenshteinDistance('book', 'back')).toBe(2);
17
+ });
18
+
19
+ test('should handle empty strings', () => {
20
+ expect(levenshteinDistance('', '')).toBe(0);
21
+ expect(levenshteinDistance('test', '')).toBe(4);
22
+ expect(levenshteinDistance('', 'test')).toBe(4);
23
+ });
24
+ });
25
+
26
+ describe('stringSimilarity function', () => {
27
+ test('should calculate correct similarity', () => {
28
+ expect(stringSimilarity('test', 'test')).toBe(1); // identical
29
+ expect(stringSimilarity('test', 'tent')).toBe(0.75); // 1 change out of 4
30
+ expect(stringSimilarity('book', 'back')).toBe(0.5); // 2 changes out of 4
31
+ });
32
+
33
+ test('should handle empty strings', () => {
34
+ expect(stringSimilarity('', '')).toBe(1); // both empty = identical
35
+ expect(stringSimilarity('test', '')).toBe(0);
36
+ expect(stringSimilarity('', 'test')).toBe(0);
37
+ });
38
+ });
39
+
40
+ describe('findMostSimilar function', () => {
41
+ test('should find most similar string', () => {
42
+ const candidates = ['apple', 'banana', 'orange', 'appl'];
43
+ expect(findMostSimilar('aple', candidates, 0.7)).toBe('apple');
44
+ });
45
+
46
+ test('should return null when no match above threshold', () => {
47
+ const candidates = ['apple', 'banana', 'orange'];
48
+ expect(findMostSimilar('xyz', candidates, 0.7)).toBeNull();
49
+ });
50
+
51
+ test('should handle empty candidates', () => {
52
+ expect(findMostSimilar('test', [], 0.7)).toBeNull();
53
+ });
54
+ });
55
+
56
+ describe('findMostSimilarWithLevenshtein function', () => {
57
+ test('should find most similar string using Levenshtein', () => {
58
+ const candidates = ['anjing', 'babi', 'kontol'];
59
+ expect(findMostSimilarWithLevenshtein('anjiing', candidates, 0.7, 2)).toBe('anjing');
60
+ });
61
+
62
+ test('should respect max distance limit', () => {
63
+ const candidates = ['anjing', 'anjiang'];
64
+ // With default max distance (3), it should match
65
+ expect(findMostSimilarWithLevenshtein('anjiiing', candidates, 0.7)).toBe('anjing');
66
+
67
+ // With stricter max distance (1), it should not match
68
+ expect(findMostSimilarWithLevenshtein('anjiiiing', candidates, 0.7, 1)).toBeNull();
69
+ });
70
+ });
71
+
72
+ describe('isPossibleProfanityVariation function', () => {
73
+ test('should detect possible profanity variations', () => {
74
+ const profanityWords = ['anjing', 'babi'];
75
+ const [isProfanity, original] = isPossibleProfanityVariation('anjiing', profanityWords, 0.8);
76
+
77
+ expect(isProfanity).toBe(true);
78
+ expect(original).toBe('anjing');
79
+ });
80
+
81
+ test('should return false for non-matches', () => {
82
+ const profanityWords = ['anjing', 'babi'];
83
+ const [isProfanity, original] = isPossibleProfanityVariation('kucing', profanityWords, 0.8);
84
+
85
+ expect(isProfanity).toBe(false);
86
+ expect(original).toBeNull();
87
+ });
88
+ });
89
+
90
+ describe('findPossibleProfanityBySimiliarity function', () => {
91
+ test('should find similar profanity words in text', () => {
92
+ const text = 'anjiing adalah binatang lucu';
93
+ const profanityWords = ['anjing', 'babi', 'kontol'];
94
+
95
+ const results = findPossibleProfanityBySimiliarity(text, profanityWords, 0.8);
96
+
97
+ expect(results.length).toBe(1);
98
+ expect(results[0].word).toBe('anjiing');
99
+ expect(results[0].original).toBe('anjing');
100
+ expect(results[0].similarity).toBeGreaterThanOrEqual(0.8);
101
+ });
102
+
103
+ test('should not match when similarity is below threshold', () => {
104
+ const text = 'kucing adalah binatang lucu';
105
+ const profanityWords = ['anjing', 'babi'];
106
+
107
+ const results = findPossibleProfanityBySimiliarity(text, profanityWords, 0.8);
108
+ expect(results.length).toBe(0);
109
+ });
110
+ });
111
+
112
+ describe('findProfanityByLevenshteinDistance function', () => {
113
+ test('should find profanity by Levenshtein distance', () => {
114
+ const text = 'anjiing adalah binatang lucu';
115
+ const profanityWords = ['anjing', 'babi', 'kontol'];
116
+
117
+ const results = findProfanityByLevenshteinDistance(text, profanityWords, 0.8, 2);
118
+
119
+ expect(results.length).toBe(1);
120
+ expect(results[0].word).toBe('anjiing');
121
+ expect(results[0].original).toBe('anjing');
122
+ expect(results[0].distance).toBe(1);
123
+ expect(results[0].similarity).toBeGreaterThanOrEqual(0.8);
124
+ });
125
+
126
+ test('should respect maximum distance', () => {
127
+ const text = 'anjiiiing adalah binatang lucu';
128
+ const profanityWords = ['anjing', 'babi'];
129
+
130
+ // With max distance 2, should not match
131
+ const resultsStrict = findProfanityByLevenshteinDistance(text, profanityWords, 0.6, 2);
132
+ expect(resultsStrict.length).toBe(0);
133
+
134
+ // With max distance 4, should match
135
+ const resultsLax = findProfanityByLevenshteinDistance(text, profanityWords, 0.6, 4);
136
+ expect(resultsLax.length).toBe(1);
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,153 @@
1
+ import {
2
+ censorWord,
3
+ normalizeText,
4
+ containsAnyWord,
5
+ containsEuphemism,
6
+ detectSplitWords,
7
+ escapeRegExp,
8
+ maskText,
9
+ splitIntoSentences,
10
+ getContextAroundIndex,
11
+ } from '../../src/utils/stringUtils';
12
+
13
+ describe('String Utils', () => {
14
+ describe('censorWord function', () => {
15
+ test('should replace entire word by default', () => {
16
+ expect(censorWord('test')).toBe('****');
17
+ });
18
+
19
+ test('should use custom replacement character', () => {
20
+ expect(censorWord('test', '#')).toBe('####');
21
+ });
22
+
23
+ test('should preserve first and last letters when specified', () => {
24
+ expect(censorWord('testing', '*', true)).toBe('t*****g');
25
+ });
26
+
27
+ test('should handle short words with keepFirstAndLast', () => {
28
+ expect(censorWord('hi', '*', true)).toBe('**');
29
+ });
30
+ });
31
+
32
+ describe('normalizeText function', () => {
33
+ test('should convert to lowercase', () => {
34
+ expect(normalizeText('TEST')).toBe('test');
35
+ });
36
+
37
+ test('should remove non-alphanumeric characters', () => {
38
+ expect(normalizeText('test!')).toBe('test');
39
+ expect(normalizeText('test#123')).toBe('test123');
40
+ });
41
+
42
+ test('should trim whitespace', () => {
43
+ expect(normalizeText(' test ')).toBe('test');
44
+ });
45
+
46
+ test('should normalize Unicode characters', () => {
47
+ expect(normalizeText('café')).toBe('cafe');
48
+ });
49
+ });
50
+
51
+ describe('containsAnyWord function', () => {
52
+ test('should detect whole words by default', () => {
53
+ expect(containsAnyWord('anjing', ['anjing'])).toBe(true);
54
+ expect(containsAnyWord('testanjing', ['anjing'])).toBe(false);
55
+ });
56
+
57
+ test('should detect substrings when specified', () => {
58
+ expect(containsAnyWord('testanjing', ['anjing'], true)).toBe(true);
59
+ });
60
+
61
+ test('should handle multiple words', () => {
62
+ expect(containsAnyWord('test anjing', ['test', 'anjing'])).toBe(true);
63
+ expect(containsAnyWord('test only', ['anjing', 'babi'])).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('containsEuphemism function', () => {
68
+ test('should detect euphemisms', () => {
69
+ expect(containsEuphemism('a****g', ['anjing'])).toBe(true);
70
+ });
71
+
72
+ test('should not match unrelated patterns', () => {
73
+ expect(containsEuphemism('a**g', ['anjing'])).toBe(false);
74
+ });
75
+
76
+ test('should handle short words properly', () => {
77
+ expect(containsEuphemism('a*b', ['ab'])).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe('detectSplitWords function', () => {
82
+ test('should detect split words', () => {
83
+ expect(detectSplitWords('a-n-j-i-n-g', ['anjing'])).toBe(true);
84
+ expect(detectSplitWords('a n j i n g', ['anjing'])).toBe(true);
85
+ });
86
+
87
+ test('should not detect unrelated words', () => {
88
+ expect(detectSplitWords('a-b-c-d', ['anjing'])).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('escapeRegExp function', () => {
93
+ test('should escape special regex characters', () => {
94
+ expect(escapeRegExp('test*')).toBe('test\\*');
95
+ expect(escapeRegExp('test.')).toBe('test\\.');
96
+ expect(escapeRegExp('test+')).toBe('test\\+');
97
+ });
98
+ });
99
+
100
+ describe('maskText function', () => {
101
+ test('should mask middle part of text', () => {
102
+ expect(maskText('password', 1, 1)).toBe('p******d');
103
+ expect(maskText('email@example.com', 2, 3)).toBe('em************com')
104
+ });
105
+
106
+ test('should handle short text', () => {
107
+ expect(maskText('ab', 1, 1)).toBe('ab');
108
+ });
109
+
110
+ test('should handle empty text', () => {
111
+ expect(maskText('', 1, 1)).toBe('');
112
+ });
113
+ });
114
+
115
+ describe('splitIntoSentences function', () => {
116
+ test('should split text into sentences', () => {
117
+ const sentences = splitIntoSentences('Hello. How are you? I am fine!');
118
+ expect(sentences.length).toBe(3);
119
+ expect(sentences[0]).toBe('Hello.');
120
+ expect(sentences[1]).toBe('How are you?');
121
+ expect(sentences[2]).toBe('I am fine!');
122
+ });
123
+
124
+ test('should handle empty text', () => {
125
+ expect(splitIntoSentences('')).toEqual([]);
126
+ });
127
+ });
128
+
129
+ describe('getContextAroundIndex function', () => {
130
+ const text = 'The quick brown fox jumps over the lazy dog';
131
+
132
+ test('should get context around index', () => {
133
+ // "fox" starts at index 16
134
+ const context = getContextAroundIndex(text, 16, 2);
135
+ expect(context).toBe('quick brown fox jumps over');
136
+ });
137
+
138
+ test('should handle index at start', () => {
139
+ const context = getContextAroundIndex(text, 0, 2);
140
+ expect(context).toBe('The quick brown');
141
+ });
142
+
143
+ test('should handle index at end', () => {
144
+ const context = getContextAroundIndex(text, text.length - 1, 2);
145
+ expect(context).toBe('the lazy dog');
146
+ });
147
+
148
+ test('should handle invalid index', () => {
149
+ expect(getContextAroundIndex(text, -1, 2)).toBe('');
150
+ expect(getContextAroundIndex(text, 100, 2)).toBe('');
151
+ });
152
+ });
153
+ });