Old paragraph 1
Old paragraph 2
/** * DomPatcher Tests * * Tests for lightweight DOM patching functionality used by LiveComponent * for efficient fragment-based partial rendering. */ import { DomPatcher, domPatcher } from '../../../../resources/js/modules/livecomponent/DomPatcher.js'; describe('DomPatcher', () => { let patcher; beforeEach(() => { patcher = new DomPatcher(); document.body.innerHTML = ''; }); describe('patchFragment', () => { it('patches a single fragment successfully', () => { // Setup container with fragment const container = document.createElement('div'); container.innerHTML = `
Count: 5
Count: 10
Old Content
Old Footer
New Content
First
'; const newElement = document.createElement('div'); newElement.innerHTML = 'First
Second
'; patcher.patchChildren(oldElement, newElement); expect(oldElement.children.length).toBe(2); expect(oldElement.children[1].textContent).toBe('Second'); }); it('removes deleted child nodes', () => { const oldElement = document.createElement('div'); oldElement.innerHTML = 'First
Second
'; const newElement = document.createElement('div'); newElement.innerHTML = 'First
'; patcher.patchChildren(oldElement, newElement); expect(oldElement.children.length).toBe(1); expect(oldElement.children[0].textContent).toBe('First'); }); it('updates text node content', () => { const oldElement = document.createElement('div'); oldElement.textContent = 'Old text'; const newElement = document.createElement('div'); newElement.textContent = 'New text'; patcher.patchChildren(oldElement, newElement); expect(oldElement.textContent).toBe('New text'); }); it('recursively patches nested elements', () => { const oldElement = document.createElement('div'); oldElement.innerHTML = 'Old
New
Content
'; const newElement = document.createElement('div'); newElement.innerHTML = 'Content'; patcher.patchChildren(oldElement, newElement); expect(oldElement.children[0].tagName).toBe('SPAN'); }); }); describe('shouldPatch', () => { it('returns false for different node types', () => { const textNode = document.createTextNode('text'); const elementNode = document.createElement('div'); expect(patcher.shouldPatch(textNode, elementNode)).toBe(false); }); it('returns true for text nodes', () => { const oldText = document.createTextNode('old'); const newText = document.createTextNode('new'); expect(patcher.shouldPatch(oldText, newText)).toBe(true); }); it('returns false for different element tags', () => { const divElement = document.createElement('div'); const spanElement = document.createElement('span'); expect(patcher.shouldPatch(divElement, spanElement)).toBe(false); }); it('returns true for same element tags', () => { const div1 = document.createElement('div'); const div2 = document.createElement('div'); expect(patcher.shouldPatch(div1, div2)).toBe(true); }); it('respects data-lc-key for element identity', () => { const element1 = document.createElement('div'); element1.setAttribute('data-lc-key', 'key-1'); const element2 = document.createElement('div'); element2.setAttribute('data-lc-key', 'key-2'); expect(patcher.shouldPatch(element1, element2)).toBe(false); }); it('patches elements with matching data-lc-key', () => { const element1 = document.createElement('div'); element1.setAttribute('data-lc-key', 'same-key'); const element2 = document.createElement('div'); element2.setAttribute('data-lc-key', 'same-key'); expect(patcher.shouldPatch(element1, element2)).toBe(true); }); it('uses id as fallback key', () => { const element1 = document.createElement('div'); element1.id = 'element-1'; const element2 = document.createElement('div'); element2.id = 'element-2'; expect(patcher.shouldPatch(element1, element2)).toBe(false); }); }); describe('preserveFocus', () => { it('preserves focused input element', () => { const container = document.createElement('div'); const input = document.createElement('input'); input.id = 'test-input'; input.value = 'Test'; container.appendChild(input); document.body.appendChild(container); input.focus(); input.setSelectionRange(2, 4); const restoreFocus = patcher.preserveFocus(container); // Simulate patching that might lose focus document.body.focus(); restoreFocus(); expect(document.activeElement).toBe(input); expect(input.selectionStart).toBe(2); expect(input.selectionEnd).toBe(4); }); it('returns no-op when no element is focused', () => { const container = document.createElement('div'); document.body.appendChild(container); const restoreFocus = patcher.preserveFocus(container); expect(typeof restoreFocus).toBe('function'); expect(() => restoreFocus()).not.toThrow(); }); it('returns no-op when focused element is outside container', () => { const container = document.createElement('div'); const outsideInput = document.createElement('input'); document.body.appendChild(container); document.body.appendChild(outsideInput); outsideInput.focus(); const restoreFocus = patcher.preserveFocus(container); expect(typeof restoreFocus).toBe('function'); expect(() => restoreFocus()).not.toThrow(); }); it('handles focus restoration failure gracefully', () => { const container = document.createElement('div'); const input = document.createElement('input'); input.name = 'test'; container.appendChild(input); document.body.appendChild(container); input.focus(); const restoreFocus = patcher.preserveFocus(container); // Remove the input before restoration container.removeChild(input); expect(() => restoreFocus()).not.toThrow(); }); }); describe('getElementSelector', () => { it('uses id as primary selector', () => { const element = document.createElement('div'); element.id = 'unique-id'; element.name = 'test-name'; expect(patcher.getElementSelector(element)).toBe('#unique-id'); }); it('uses name as fallback selector', () => { const element = document.createElement('input'); element.name = 'field-name'; expect(patcher.getElementSelector(element)).toBe('[name="field-name"]'); }); it('uses data-lc-key as selector', () => { const element = document.createElement('div'); element.setAttribute('data-lc-key', 'item-123'); expect(patcher.getElementSelector(element)).toBe('[data-lc-key="item-123"]'); }); it('falls back to tag name', () => { const element = document.createElement('span'); expect(patcher.getElementSelector(element)).toBe('span'); }); }); describe('singleton instance', () => { it('exports singleton domPatcher instance', () => { expect(domPatcher).toBeInstanceOf(DomPatcher); }); it('singleton is the same instance', () => { const instance1 = domPatcher; const instance2 = domPatcher; expect(instance1).toBe(instance2); }); }); describe('complex patching scenarios', () => { it('patches nested fragments with multiple changes', () => { const container = document.createElement('div'); container.innerHTML = `Old paragraph 1
Old paragraph 2
New paragraph 1
New paragraph 2
New paragraph 3
Content
Content