Files
michaelschiemer/tests/javascript/modules/livecomponent/DomPatcher.test.js
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

513 lines
19 KiB
JavaScript

/**
* 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 = `
<div data-lc-fragment="user-stats">
<h3>Statistics</h3>
<p>Count: 5</p>
</div>
`;
document.body.appendChild(container);
// Patch fragment with new content
const newHtml = `
<div data-lc-fragment="user-stats">
<h3>Statistics</h3>
<p>Count: 10</p>
</div>
`;
const result = patcher.patchFragment(container, 'user-stats', newHtml);
expect(result).toBe(true);
expect(container.querySelector('[data-lc-fragment="user-stats"] p').textContent).toBe('Count: 10');
});
it('returns false when fragment not found', () => {
const container = document.createElement('div');
container.innerHTML = '<div>No fragments here</div>';
const result = patcher.patchFragment(container, 'nonexistent', '<div data-lc-fragment="nonexistent">Test</div>');
expect(result).toBe(false);
});
it('returns false for invalid HTML', () => {
const container = document.createElement('div');
container.innerHTML = '<div data-lc-fragment="test">Test</div>';
const result = patcher.patchFragment(container, 'test', '');
expect(result).toBe(false);
});
it('returns false when fragment name mismatch', () => {
const container = document.createElement('div');
container.innerHTML = '<div data-lc-fragment="fragment-a">Test</div>';
const newHtml = '<div data-lc-fragment="fragment-b">Updated</div>';
const result = patcher.patchFragment(container, 'fragment-a', newHtml);
expect(result).toBe(false);
});
});
describe('patchFragments', () => {
it('patches multiple fragments at once', () => {
const container = document.createElement('div');
container.innerHTML = `
<div data-lc-fragment="header">
<h1>Old Header</h1>
</div>
<div data-lc-fragment="content">
<p>Old Content</p>
</div>
<div data-lc-fragment="footer">
<p>Old Footer</p>
</div>
`;
const fragments = {
'header': '<div data-lc-fragment="header"><h1>New Header</h1></div>',
'content': '<div data-lc-fragment="content"><p>New Content</p></div>'
};
const results = patcher.patchFragments(container, fragments);
expect(results.header).toBe(true);
expect(results.content).toBe(true);
expect(container.querySelector('[data-lc-fragment="header"] h1').textContent).toBe('New Header');
expect(container.querySelector('[data-lc-fragment="content"] p').textContent).toBe('New Content');
expect(container.querySelector('[data-lc-fragment="footer"] p').textContent).toBe('Old Footer');
});
it('returns success status for each fragment', () => {
const container = document.createElement('div');
container.innerHTML = '<div data-lc-fragment="exists">Test</div>';
const fragments = {
'exists': '<div data-lc-fragment="exists">Updated</div>',
'missing': '<div data-lc-fragment="missing">Not found</div>'
};
const results = patcher.patchFragments(container, fragments);
expect(results.exists).toBe(true);
expect(results.missing).toBe(false);
});
});
describe('patchAttributes', () => {
it('updates changed attributes', () => {
const oldElement = document.createElement('div');
oldElement.setAttribute('class', 'old-class');
oldElement.setAttribute('data-value', '5');
const newElement = document.createElement('div');
newElement.setAttribute('class', 'new-class');
newElement.setAttribute('data-value', '10');
patcher.patchAttributes(oldElement, newElement);
expect(oldElement.getAttribute('class')).toBe('new-class');
expect(oldElement.getAttribute('data-value')).toBe('10');
});
it('adds new attributes', () => {
const oldElement = document.createElement('div');
oldElement.setAttribute('class', 'test');
const newElement = document.createElement('div');
newElement.setAttribute('class', 'test');
newElement.setAttribute('data-new', 'value');
patcher.patchAttributes(oldElement, newElement);
expect(oldElement.getAttribute('data-new')).toBe('value');
});
it('removes attributes that no longer exist', () => {
const oldElement = document.createElement('div');
oldElement.setAttribute('class', 'test');
oldElement.setAttribute('data-old', 'value');
const newElement = document.createElement('div');
newElement.setAttribute('class', 'test');
patcher.patchAttributes(oldElement, newElement);
expect(oldElement.hasAttribute('data-old')).toBe(false);
expect(oldElement.getAttribute('class')).toBe('test');
});
it('does not update unchanged attributes', () => {
const oldElement = document.createElement('div');
oldElement.setAttribute('class', 'unchanged');
const setAttributeSpy = jest.spyOn(oldElement, 'setAttribute');
const newElement = document.createElement('div');
newElement.setAttribute('class', 'unchanged');
patcher.patchAttributes(oldElement, newElement);
expect(setAttributeSpy).not.toHaveBeenCalledWith('class', 'unchanged');
});
});
describe('patchChildren', () => {
it('appends new child nodes', () => {
const oldElement = document.createElement('div');
oldElement.innerHTML = '<p>First</p>';
const newElement = document.createElement('div');
newElement.innerHTML = '<p>First</p><p>Second</p>';
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 = '<p>First</p><p>Second</p>';
const newElement = document.createElement('div');
newElement.innerHTML = '<p>First</p>';
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 = '<div><p>Old</p></div>';
const newElement = document.createElement('div');
newElement.innerHTML = '<div><p>New</p></div>';
patcher.patchChildren(oldElement, newElement);
expect(oldElement.querySelector('p').textContent).toBe('New');
});
it('replaces elements with different tags', () => {
const oldElement = document.createElement('div');
oldElement.innerHTML = '<p>Content</p>';
const newElement = document.createElement('div');
newElement.innerHTML = '<span>Content</span>';
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 = `
<div data-lc-fragment="card">
<h2 class="old-title">Old Title</h2>
<div class="content">
<p>Old paragraph 1</p>
<p>Old paragraph 2</p>
</div>
<button data-action="old">Old Button</button>
</div>
`;
const newHtml = `
<div data-lc-fragment="card">
<h2 class="new-title">New Title</h2>
<div class="content">
<p>New paragraph 1</p>
<p>New paragraph 2</p>
<p>New paragraph 3</p>
</div>
<button data-action="new">New Button</button>
</div>
`;
patcher.patchFragment(container, 'card', newHtml);
const fragment = container.querySelector('[data-lc-fragment="card"]');
expect(fragment.querySelector('h2').textContent).toBe('New Title');
expect(fragment.querySelector('h2').className).toBe('new-title');
expect(fragment.querySelectorAll('.content p').length).toBe(3);
expect(fragment.querySelector('button').getAttribute('data-action')).toBe('new');
});
it('handles whitespace and formatting changes', () => {
const container = document.createElement('div');
container.innerHTML = '<div data-lc-fragment="test"><p>Content</p></div>';
const newHtml = `
<div data-lc-fragment="test">
<p>Content</p>
</div>
`;
const result = patcher.patchFragment(container, 'test', newHtml);
expect(result).toBe(true);
expect(container.querySelector('p').textContent).toBe('Content');
});
it('preserves event listeners on patched elements', () => {
const container = document.createElement('div');
container.innerHTML = '<div data-lc-fragment="test"><button id="btn">Click</button></div>';
document.body.appendChild(container);
const button = container.querySelector('#btn');
let clicked = false;
button.addEventListener('click', () => { clicked = true; });
const newHtml = '<div data-lc-fragment="test"><button id="btn">Updated</button></div>';
patcher.patchFragment(container, 'test', newHtml);
// Event listener is preserved because element is patched, not replaced
container.querySelector('#btn').click();
expect(clicked).toBe(true);
});
});
describe('performance characteristics', () => {
it('patches large fragment efficiently', () => {
const container = document.createElement('div');
const itemCount = 100;
// Build initial list
const items = [];
for (let i = 0; i < itemCount; i++) {
items.push(`<li data-lc-key="item-${i}">Item ${i}</li>`);
}
container.innerHTML = `<ul data-lc-fragment="list">${items.join('')}</ul>`;
// Update half the items
const updatedItems = [];
for (let i = 0; i < itemCount; i++) {
if (i % 2 === 0) {
updatedItems.push(`<li data-lc-key="item-${i}">Updated Item ${i}</li>`);
} else {
updatedItems.push(`<li data-lc-key="item-${i}">Item ${i}</li>`);
}
}
const newHtml = `<ul data-lc-fragment="list">${updatedItems.join('')}</ul>`;
const startTime = performance.now();
patcher.patchFragment(container, 'list', newHtml);
const duration = performance.now() - startTime;
// Should complete in reasonable time (< 100ms for 100 items)
expect(duration).toBeLessThan(100);
// Verify updates applied
const firstItem = container.querySelector('[data-lc-key="item-0"]');
expect(firstItem.textContent).toBe('Updated Item 0');
});
});
});