')).toBe('<div>');
+ expect(escapeHtml('&')).toBe('&');
+ expect(escapeHtml('"hello"')).toBe('"hello"');
+ expect(escapeHtml("'hello'")).toBe(''hello'');
+ });
+
+ it('should handle mixed content', () => {
+ expect(escapeHtml(''))
+ .toBe('<script>alert("XSS")</script>');
+ });
+
+ it('should handle empty string', () => {
+ expect(escapeHtml('')).toBe('');
+ });
+
+ it('should not escape safe text', () => {
+ expect(escapeHtml('hello world')).toBe('hello world');
+ });
+});
+
+describe('mergeOverlappingMatches', () => {
+ it('should return empty array for empty input', () => {
+ expect(mergeOverlappingMatches([])).toEqual([]);
+ });
+
+ it('should return single match unchanged', () => {
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ expect(mergeOverlappingMatches(matches)).toEqual(matches);
+ });
+
+ it('should merge overlapping matches', () => {
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 3, end: 8, text: 'lo wo', type: 'exact' }
+ ];
+ const result = mergeOverlappingMatches(matches);
+ expect(result).toHaveLength(1);
+ expect(result[0].start).toBe(0);
+ expect(result[0].end).toBe(8);
+ });
+
+ it('should merge adjacent matches', () => {
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 5, end: 11, text: ' world', type: 'exact' }
+ ];
+ const result = mergeOverlappingMatches(matches);
+ expect(result).toHaveLength(1);
+ expect(result[0].start).toBe(0);
+ expect(result[0].end).toBe(11);
+ });
+
+ it('should keep separate non-overlapping matches', () => {
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 10, end: 15, text: 'world', type: 'exact' }
+ ];
+ const result = mergeOverlappingMatches(matches);
+ expect(result).toHaveLength(2);
+ expect(result[0].start).toBe(0);
+ expect(result[0].end).toBe(5);
+ expect(result[1].start).toBe(10);
+ expect(result[1].end).toBe(15);
+ });
+
+ it('should handle unsorted matches', () => {
+ const matches: MatchInfo[] = [
+ { start: 10, end: 15, text: 'world', type: 'exact' },
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ const result = mergeOverlappingMatches(matches);
+ expect(result).toHaveLength(2);
+ expect(result[0].start).toBe(0);
+ expect(result[1].start).toBe(10);
+ });
+
+ it('should merge multiple overlapping matches', () => {
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 3, end: 8, text: 'lo wo', type: 'exact' },
+ { start: 6, end: 11, text: 'world', type: 'exact' }
+ ];
+ const result = mergeOverlappingMatches(matches);
+ expect(result).toHaveLength(1);
+ expect(result[0].start).toBe(0);
+ expect(result[0].end).toBe(11);
+ });
+});
+
+describe('highlightWithMatchInfo', () => {
+ it('should return escaped text with no matches', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [];
+ expect(highlightWithMatchInfo(text, matches)).toBe('hello world');
+ });
+
+ it('should highlight single match', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('
hello world');
+ });
+
+ it('should highlight multiple matches', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 6, end: 11, text: 'world', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('
hello world');
+ });
+
+ it('should use custom className', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches, { className: 'custom-highlight' });
+ expect(result).toBe('
hello world');
+ });
+
+ it('should escape HTML in text', () => {
+ const text = '
hello
';
+ const matches: MatchInfo[] = [
+ { start: 5, end: 10, text: 'hello', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('<div>
hello</div>');
+ });
+
+ it('should handle empty text', () => {
+ const text = '';
+ const matches: MatchInfo[] = [];
+ expect(highlightWithMatchInfo(text, matches)).toBe('');
+ });
+
+ it('should merge overlapping matches before highlighting', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' },
+ { start: 3, end: 8, text: 'lo wo', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('
hello world');
+ });
+
+ it('should truncate long text with maxLength option', () => {
+ const text = 'This is a very long text that should be truncated when it exceeds the maximum length';
+ const matches: MatchInfo[] = [
+ { start: 10, end: 14, text: 'very', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches, { maxLength: 50, contextLength: 10 });
+ expect(result.length).toBeLessThan(text.length);
+ expect(result).toContain('very');
+ expect(result).toContain('...');
+ });
+
+ it('should handle match at the beginning of text', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('
hello world');
+ });
+
+ it('should handle match at the end of text', () => {
+ const text = 'hello world';
+ const matches: MatchInfo[] = [
+ { start: 6, end: 11, text: 'world', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('hello
world');
+ });
+
+ it('should handle entire text as match', () => {
+ const text = 'hello';
+ const matches: MatchInfo[] = [
+ { start: 0, end: 5, text: 'hello', type: 'exact' }
+ ];
+ const result = highlightWithMatchInfo(text, matches);
+ expect(result).toBe('
hello');
+ });
+});
+
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index d894a3f..d5fb2f7 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -51,8 +51,46 @@ describe('utils', () => {
});
describe('isJSON', () => {
- it('returns true if is JSON object', () => {
+ it('returns true for plain objects', () => {
expect(isJSON({ foo: 'bar' })).toBe(true);
+ expect(isJSON({})).toBe(true);
+ expect(isJSON({ nested: { key: 'value' } })).toBe(true);
+ });
+
+ it('returns true for arrays', () => {
+ expect(isJSON([])).toBe(true);
+ expect(isJSON([1, 2, 3])).toBe(true);
+ expect(isJSON([{ foo: 'bar' }])).toBe(true);
+ });
+
+ it('returns false for null', () => {
+ expect(isJSON(null)).toBe(false);
+ });
+
+ it('returns false for undefined', () => {
+ expect(isJSON(undefined)).toBe(false);
+ });
+
+ it('returns false for primitives', () => {
+ expect(isJSON(42)).toBe(false);
+ expect(isJSON(0)).toBe(false);
+ expect(isJSON('string')).toBe(false);
+ expect(isJSON('')).toBe(false);
+ expect(isJSON(true)).toBe(false);
+ expect(isJSON(false)).toBe(false);
+ });
+
+ it('returns true for Date objects', () => {
+ expect(isJSON(new Date())).toBe(true);
+ });
+
+ it('returns true for RegExp objects', () => {
+ expect(isJSON(/regex/)).toBe(true);
+ });
+
+ it('returns false for functions', () => {
+ expect(isJSON(() => {})).toBe(false);
+ expect(isJSON(function() {})).toBe(false);
});
});
diff --git a/vite.config.ts b/vite.config.ts
index 02d7fc6..13a6914 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -25,7 +25,7 @@ export default defineConfig({
test: {
coverage: {
provider: 'v8',
- reporter: ['text', 'json', 'html', 'lcov'],
+ reporter: ['text', 'lcov'],
include: ['src/**/*.ts'],
exclude: [
'**/*.d.ts',