mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Add test coverage for InputService.
This commit is contained in:
parent
9b22f61bbe
commit
eab7e77303
1 changed files with 711 additions and 4 deletions
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { InputService } from '../../Services/InputService';
|
||||
import { SearchTrigger } from '../../Enums/SearchTrigger';
|
||||
|
||||
/**
|
||||
* UNIT TESTS
|
||||
*
|
||||
* These tests cover the straightforward methods of InputService that don't
|
||||
* require complex DOM/Selection API mocking. Methods involving cursor manipulation
|
||||
* and Selection/Range APIs are not covered here due to testing complexity.
|
||||
* These tests cover both simple methods and DOM/Selection API methods using JSDOM.
|
||||
* JSDOM provides the DOM environment needed for cursor manipulation and Range/Selection APIs.
|
||||
*/
|
||||
|
||||
describe('InputService', () => {
|
||||
|
|
@ -330,4 +330,711 @@ describe('InputService', () => {
|
|||
expect(service.stripHtml(html)).toBe('Text beforeInside tagText after');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlainTextFromClipboard', () => {
|
||||
it('should extract text/plain from DataTransfer', () => {
|
||||
const mockClipboardData = {
|
||||
getData: vi.fn((type: string) => {
|
||||
if (type === 'text/plain') return 'Plain text content';
|
||||
return '';
|
||||
})
|
||||
} as unknown as DataTransfer;
|
||||
|
||||
const result = service.getPlainTextFromClipboard(mockClipboardData);
|
||||
expect(result).toBe('Plain text content');
|
||||
expect(mockClipboardData.getData).toHaveBeenCalledWith('text/plain');
|
||||
});
|
||||
|
||||
it('should return empty string for null clipboard', () => {
|
||||
const result = service.getPlainTextFromClipboard(null);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when getData returns empty', () => {
|
||||
const mockClipboardData = {
|
||||
getData: vi.fn(() => '')
|
||||
} as unknown as DataTransfer;
|
||||
|
||||
const result = service.getPlainTextFromClipboard(mockClipboardData);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle clipboard with no text/plain data', () => {
|
||||
const mockClipboardData = {
|
||||
getData: vi.fn(() => null)
|
||||
} as unknown as DataTransfer;
|
||||
|
||||
const result = service.getPlainTextFromClipboard(mockClipboardData);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasUnauthorizedHTML', () => {
|
||||
let element: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
element.contentEditable = 'true';
|
||||
});
|
||||
|
||||
it('should return false for text-only content', () => {
|
||||
element.textContent = 'Plain text content';
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for BR tags (browser auto-insert)', () => {
|
||||
const br = document.createElement('br');
|
||||
element.appendChild(br);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for DIV with only text content', () => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Text in div';
|
||||
element.appendChild(div);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for DIV with BR tags', () => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Line 1';
|
||||
const br = document.createElement('br');
|
||||
div.appendChild(br);
|
||||
div.appendChild(document.createTextNode('Line 2'));
|
||||
element.appendChild(div);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for search trigger elements', () => {
|
||||
const trigger = document.createElement('span');
|
||||
trigger.className = 'search-trigger';
|
||||
trigger.textContent = '#tag';
|
||||
element.appendChild(trigger);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for mixed text, BR, DIV, and search triggers', () => {
|
||||
element.appendChild(document.createTextNode('Text '));
|
||||
const trigger = document.createElement('span');
|
||||
trigger.className = 'search-trigger';
|
||||
element.appendChild(trigger);
|
||||
element.appendChild(document.createTextNode(' more text'));
|
||||
const br = document.createElement('br');
|
||||
element.appendChild(br);
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Div content';
|
||||
element.appendChild(div);
|
||||
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for SPAN tags (non-search-trigger)', () => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'Regular span';
|
||||
element.appendChild(span);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for STRONG tags', () => {
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = 'Bold text';
|
||||
element.appendChild(strong);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for IMG tags', () => {
|
||||
const img = document.createElement('img');
|
||||
img.src = 'test.png';
|
||||
element.appendChild(img);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for SCRIPT tags', () => {
|
||||
const script = document.createElement('script');
|
||||
script.textContent = 'alert("test")';
|
||||
element.appendChild(script);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for A (anchor) tags', () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = 'http://example.com';
|
||||
a.textContent = 'Link';
|
||||
element.appendChild(a);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect unauthorized HTML in nested DIVs', () => {
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'Nested span';
|
||||
div.appendChild(span);
|
||||
element.appendChild(div);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for deeply nested unauthorized elements', () => {
|
||||
const div1 = document.createElement('div');
|
||||
const div2 = document.createElement('div');
|
||||
const unauthorized = document.createElement('em');
|
||||
unauthorized.textContent = 'Italic';
|
||||
div2.appendChild(unauthorized);
|
||||
div1.appendChild(div2);
|
||||
element.appendChild(div1);
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty element', () => {
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle element with only whitespace', () => {
|
||||
element.textContent = ' \n ';
|
||||
expect(service.hasUnauthorizedHTML(element)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DOM Manipulation Methods', () => {
|
||||
let element: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a contentEditable element
|
||||
element = document.createElement('div');
|
||||
element.contentEditable = 'true';
|
||||
element.textContent = 'Hello World';
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
describe('getCursorPosition', () => {
|
||||
it('should return -1 when no selection exists', () => {
|
||||
// Clear any existing selection
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return cursor position at start of text', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 0);
|
||||
range.setEnd(textNode, 0);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(0);
|
||||
});
|
||||
|
||||
it('should return cursor position in middle of text', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 6); // After "Hello "
|
||||
range.setEnd(textNode, 6);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(6);
|
||||
});
|
||||
|
||||
it('should return cursor position at end of text', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 11); // After "Hello World"
|
||||
range.setEnd(textNode, 11);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(11);
|
||||
});
|
||||
|
||||
it('should handle nested elements', () => {
|
||||
element.innerHTML = '';
|
||||
element.appendChild(document.createTextNode('Hello '));
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'World';
|
||||
element.appendChild(span);
|
||||
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const spanTextNode = span.firstChild as Text;
|
||||
|
||||
range.setStart(spanTextNode, 2); // "Wo" - position 8 overall
|
||||
range.setEnd(spanTextNode, 2);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCursorPosition', () => {
|
||||
it('should return false for non-contentEditable element', () => {
|
||||
const nonEditable = document.createElement('div');
|
||||
nonEditable.textContent = 'Not editable';
|
||||
document.body.appendChild(nonEditable);
|
||||
|
||||
const result = service.setCursorPosition(nonEditable, 5);
|
||||
expect(result).toBe(false);
|
||||
|
||||
document.body.removeChild(nonEditable);
|
||||
});
|
||||
|
||||
it('should set cursor at start of text', () => {
|
||||
const result = service.setCursorPosition(element, 0);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(0);
|
||||
});
|
||||
|
||||
it('should set cursor in middle of text', () => {
|
||||
const result = service.setCursorPosition(element, 6);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(6);
|
||||
});
|
||||
|
||||
it('should set cursor at end of text', () => {
|
||||
const result = service.setCursorPosition(element, 11);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(11);
|
||||
});
|
||||
|
||||
it('should clamp position beyond text length', () => {
|
||||
const result = service.setCursorPosition(element, 999);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(11); // Max length
|
||||
});
|
||||
|
||||
it('should set cursor from end with fromEnd=true', () => {
|
||||
const result = service.setCursorPosition(element, 5, true);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(6); // 11 - 5 = 6
|
||||
});
|
||||
|
||||
it('should set cursor at start when fromEnd position exceeds length', () => {
|
||||
const result = service.setCursorPosition(element, 999, true);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle nested elements', () => {
|
||||
element.innerHTML = '';
|
||||
element.appendChild(document.createTextNode('Hello '));
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'World';
|
||||
element.appendChild(span);
|
||||
|
||||
const result = service.setCursorPosition(element, 8);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const position = service.getCursorPosition(element);
|
||||
expect(position).toBe(8);
|
||||
});
|
||||
|
||||
it('should focus the element when setting cursor', () => {
|
||||
const focusSpy = vi.spyOn(element, 'focus');
|
||||
service.setCursorPosition(element, 5);
|
||||
expect(focusSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertTextAtCursor', () => {
|
||||
it('should insert text at cursor position', () => {
|
||||
service.setCursorPosition(element, 6); // After "Hello "
|
||||
service.insertTextAtCursor('Beautiful ', element);
|
||||
|
||||
expect(element.textContent).toBe('Hello Beautiful World');
|
||||
});
|
||||
|
||||
it('should replace selected text', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 6);
|
||||
range.setEnd(textNode, 11); // Select "World"
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
service.insertTextAtCursor('Universe', element);
|
||||
expect(element.textContent).toBe('Hello Universe');
|
||||
});
|
||||
|
||||
it('should not insert when no selection exists', () => {
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
|
||||
const originalText = element.textContent;
|
||||
service.insertTextAtCursor('Test', element);
|
||||
expect(element.textContent).toBe(originalText);
|
||||
});
|
||||
|
||||
it('should warn and not insert for non-contentEditable element', () => {
|
||||
const nonEditable = document.createElement('div');
|
||||
nonEditable.textContent = 'Not editable';
|
||||
document.body.appendChild(nonEditable);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn');
|
||||
service.insertTextAtCursor('Test', nonEditable);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Element must be contenteditable');
|
||||
document.body.removeChild(nonEditable);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertElementAtCursor', () => {
|
||||
it('should insert element at cursor position', () => {
|
||||
service.setCursorPosition(element, 6); // After "Hello "
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'Beautiful ';
|
||||
service.insertElementAtCursor(span, element);
|
||||
|
||||
expect(element.textContent).toBe('Hello Beautiful World');
|
||||
expect(element.querySelector('span')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should insert search trigger element', () => {
|
||||
service.setCursorPosition(element, 6);
|
||||
|
||||
const trigger = document.createElement('span');
|
||||
trigger.className = 'search-trigger';
|
||||
trigger.textContent = '#tag';
|
||||
service.insertElementAtCursor(trigger, element);
|
||||
|
||||
expect(element.textContent).toBe('Hello #tagWorld');
|
||||
expect(element.querySelector('.search-trigger')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should replace selected text with element', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 6);
|
||||
range.setEnd(textNode, 11); // Select "World"
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'Universe';
|
||||
service.insertElementAtCursor(span, element);
|
||||
|
||||
expect(element.textContent).toBe('Hello Universe');
|
||||
});
|
||||
|
||||
it('should not insert when no selection exists', () => {
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
|
||||
const childrenBefore = element.childNodes.length;
|
||||
const span = document.createElement('span');
|
||||
service.insertElementAtCursor(span, element);
|
||||
expect(element.childNodes.length).toBe(childrenBefore);
|
||||
});
|
||||
|
||||
it('should warn and not insert for non-contentEditable element', () => {
|
||||
const nonEditable = document.createElement('div');
|
||||
nonEditable.textContent = 'Not editable';
|
||||
document.body.appendChild(nonEditable);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn');
|
||||
const span = document.createElement('span');
|
||||
service.insertElementAtCursor(span, nonEditable);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Element must be contenteditable');
|
||||
document.body.removeChild(nonEditable);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTextRange', () => {
|
||||
it('should delete text range in middle', () => {
|
||||
service.deleteTextRange(6, 11, element); // Delete "World"
|
||||
expect(element.textContent).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should delete from start', () => {
|
||||
service.deleteTextRange(0, 6, element); // Delete "Hello "
|
||||
expect(element.textContent).toBe('World');
|
||||
});
|
||||
|
||||
it('should delete entire content', () => {
|
||||
service.deleteTextRange(0, 11, element);
|
||||
expect(element.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should delete single character', () => {
|
||||
service.deleteTextRange(5, 6, element); // Delete space
|
||||
expect(element.textContent).toBe('HelloWorld');
|
||||
});
|
||||
|
||||
it('should set cursor position after deletion', () => {
|
||||
service.deleteTextRange(6, 11, element); // Delete "World"
|
||||
const cursorPos = service.getCursorPosition(element);
|
||||
expect(cursorPos).toBe(6);
|
||||
});
|
||||
|
||||
it('should handle nested elements', () => {
|
||||
element.innerHTML = '';
|
||||
element.appendChild(document.createTextNode('Hello '));
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'World';
|
||||
element.appendChild(span);
|
||||
|
||||
service.deleteTextRange(6, 9, element); // Delete "Wor"
|
||||
expect(element.textContent).toBe('Hello ld');
|
||||
});
|
||||
|
||||
it('should warn for non-contentEditable element', () => {
|
||||
const nonEditable = document.createElement('div');
|
||||
nonEditable.textContent = 'Not editable';
|
||||
document.body.appendChild(nonEditable);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn');
|
||||
service.deleteTextRange(0, 3, nonEditable);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Element must be contenteditable');
|
||||
expect(nonEditable.textContent).toBe('Not editable'); // Unchanged
|
||||
document.body.removeChild(nonEditable);
|
||||
});
|
||||
|
||||
it('should handle invalid range gracefully', () => {
|
||||
const originalText = element.textContent;
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error');
|
||||
|
||||
// Try to delete with reversed positions (though implementation may handle this)
|
||||
service.deleteTextRange(999, 1000, element);
|
||||
|
||||
// Text should either be unchanged or empty depending on implementation
|
||||
// We just verify it doesn't crash
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviousNode', () => {
|
||||
it('should return null when no selection exists', () => {
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when cursor is at start', () => {
|
||||
service.setCursorPosition(element, 0);
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when cursor is in text node', () => {
|
||||
service.setCursorPosition(element, 5);
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return search trigger element when cursor is after it', () => {
|
||||
element.innerHTML = 'Text ';
|
||||
const trigger = document.createElement('span');
|
||||
trigger.className = 'search-trigger';
|
||||
trigger.textContent = '#tag';
|
||||
element.appendChild(trigger);
|
||||
element.appendChild(document.createTextNode(' more'));
|
||||
|
||||
// Position cursor right after the trigger element
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
range.setStart(element, 2); // After text node and trigger
|
||||
range.setEnd(element, 2);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBe(trigger);
|
||||
});
|
||||
|
||||
it('should return null when previous element is not a search trigger', () => {
|
||||
element.innerHTML = 'Text ';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'regular span';
|
||||
element.appendChild(span);
|
||||
element.appendChild(document.createTextNode(' more'));
|
||||
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
range.setStart(element, 2); // After text node and span
|
||||
range.setEnd(element, 2);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when selection is not collapsed', () => {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textNode = element.firstChild as Text;
|
||||
|
||||
range.setStart(textNode, 0);
|
||||
range.setEnd(textNode, 5); // Select "Hello"
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
|
||||
const result = service.getPreviousNode();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeAtCursorPosition', () => {
|
||||
it('should return node at cursor position', () => {
|
||||
service.setCursorPosition(element, 5);
|
||||
const node = service.getNodeAtCursorPosition();
|
||||
|
||||
// Should return the parent element or text node
|
||||
expect(node).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return null when no selection exists', () => {
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
|
||||
const node = service.getNodeAtCursorPosition();
|
||||
// JSDOM may return an empty Selection, so node might be null or a node
|
||||
// We just verify it doesn't crash
|
||||
expect(node === null || node instanceof Node).toBe(true);
|
||||
});
|
||||
|
||||
it('should return parent node for text nodes', () => {
|
||||
service.setCursorPosition(element, 5);
|
||||
const node = service.getNodeAtCursorPosition();
|
||||
|
||||
// Since cursor is in text, it should return the parent (element)
|
||||
expect(node?.nodeType).toBe(Node.ELEMENT_NODE);
|
||||
});
|
||||
|
||||
it('should handle nested elements', () => {
|
||||
element.innerHTML = '';
|
||||
element.appendChild(document.createTextNode('Hello '));
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'World';
|
||||
element.appendChild(span);
|
||||
|
||||
service.setCursorPosition(element, 8); // Inside span
|
||||
const node = service.getNodeAtCursorPosition();
|
||||
|
||||
expect(node).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeToPlainText', () => {
|
||||
it('should remove all HTML and keep plain text', () => {
|
||||
element.innerHTML = 'Hello <strong>World</strong>';
|
||||
service.setCursorPosition(element, 5);
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('Hello World');
|
||||
expect(element.innerHTML).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should preserve cursor position after sanitization', () => {
|
||||
element.innerHTML = 'Hello <strong>Bold</strong> World';
|
||||
service.setCursorPosition(element, 11); // After "Hello Bold "
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
const cursorPos = service.getCursorPosition(element);
|
||||
expect(cursorPos).toBe(11);
|
||||
});
|
||||
|
||||
it('should handle cursor at start', () => {
|
||||
element.innerHTML = '<em>Italic</em> text';
|
||||
service.setCursorPosition(element, 0);
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('Italic text');
|
||||
const cursorPos = service.getCursorPosition(element);
|
||||
expect(cursorPos).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle cursor at end', () => {
|
||||
element.innerHTML = 'Text <span>with span</span>';
|
||||
const textLength = element.textContent?.length || 0;
|
||||
service.setCursorPosition(element, textLength);
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('Text with span');
|
||||
const cursorPos = service.getCursorPosition(element);
|
||||
expect(cursorPos).toBe(14);
|
||||
});
|
||||
|
||||
it('should handle empty element', () => {
|
||||
element.innerHTML = '';
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should handle element with only HTML tags', () => {
|
||||
element.innerHTML = '<br><br>';
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('should remove search trigger elements but keep text', () => {
|
||||
element.innerHTML = 'Query ';
|
||||
const trigger = document.createElement('span');
|
||||
trigger.className = 'search-trigger';
|
||||
trigger.textContent = '#tag';
|
||||
element.appendChild(trigger);
|
||||
element.appendChild(document.createTextNode(' text'));
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('Query #tag text');
|
||||
expect(element.querySelector('.search-trigger')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle nested HTML structures', () => {
|
||||
element.innerHTML = '<div><p>Nested <strong>content</strong></p></div>';
|
||||
service.setCursorPosition(element, 7); // After "Nested "
|
||||
|
||||
service.sanitizeToPlainText(element);
|
||||
|
||||
expect(element.textContent).toBe('Nested content');
|
||||
const cursorPos = service.getCursorPosition(element);
|
||||
expect(cursorPos).toBe(7);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue