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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,388 @@
/**
* E2E Tests for LiveComponent Fragment Rendering
*
* Tests fragment-based partial rendering in real browser environment.
* Verifies that DomPatcher correctly updates specific DOM fragments
* without full page re-render.
*
* Run with: npx playwright test fragment-rendering.spec.js
*/
import { test, expect } from '@playwright/test';
test.describe('LiveComponent Fragment Rendering', () => {
test.beforeEach(async ({ page }) => {
// Navigate to test page (assumes local development server running)
await page.goto('https://localhost/livecomponents/test/counter');
// Wait for LiveComponent to initialize
await page.waitForFunction(() => window.LiveComponents !== undefined);
});
test('should patch single fragment without full re-render', async ({ page }) => {
// Get initial HTML of container
const initialHTML = await page.evaluate(() => {
return document.querySelector('[data-component-id="counter:main"]').outerHTML;
});
// Click increment button (triggers fragment update)
await page.click('[data-action="increment"]');
// Wait for fragment update
await page.waitForTimeout(100);
// Get updated HTML
const updatedHTML = await page.evaluate(() => {
return document.querySelector('[data-component-id="counter:main"]').outerHTML;
});
// Verify only counter value fragment changed
const counterValue = await page.textContent('[data-lc-fragment="counter-value"]');
expect(counterValue).toContain('1');
// Verify container structure unchanged (only fragment patched)
expect(updatedHTML).not.toBe(initialHTML);
});
test('should update multiple fragments simultaneously', async ({ page }) => {
// Navigate to shopping cart test page
await page.goto('https://localhost/livecomponents/test/shopping-cart');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Add item to cart (updates cart-items and cart-total fragments)
await page.click('[data-action="addItem"]');
await page.waitForTimeout(100);
// Verify cart items fragment updated
const cartItems = await page.$$('[data-lc-fragment="cart-items"] .cart-item');
expect(cartItems.length).toBe(1);
// Verify cart total fragment updated
const cartTotal = await page.textContent('[data-lc-fragment="cart-total"]');
expect(cartTotal).toContain('€');
});
test('should preserve focus state during fragment update', async ({ page }) => {
// Navigate to form test page
await page.goto('https://localhost/livecomponents/test/form');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Focus on input field
const input = page.locator('input[name="username"]');
await input.focus();
await input.fill('test');
await input.evaluate(el => el.setSelectionRange(2, 4)); // Select "st"
// Trigger fragment update (e.g., validation message)
await page.click('[data-action="validate"]');
await page.waitForTimeout(100);
// Verify focus preserved
const focusedElement = await page.evaluate(() => document.activeElement.name);
expect(focusedElement).toBe('username');
// Verify selection preserved
const selection = await input.evaluate(el => ({
start: el.selectionStart,
end: el.selectionEnd
}));
expect(selection).toEqual({ start: 2, end: 4 });
});
test('should handle nested fragment updates', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/nested-fragments');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Parent fragment contains child fragments
const parentFragment = page.locator('[data-lc-fragment="parent"]');
const childFragment1 = page.locator('[data-lc-fragment="child-1"]');
const childFragment2 = page.locator('[data-lc-fragment="child-2"]');
// Trigger update that affects both child fragments
await page.click('[data-action="updateChildren"]');
await page.waitForTimeout(100);
// Verify both child fragments updated
await expect(childFragment1).toContainText('Updated Child 1');
await expect(childFragment2).toContainText('Updated Child 2');
// Verify parent structure unchanged
await expect(parentFragment).toBeVisible();
});
test('should fall back to full render if fragments not specified', async ({ page }) => {
// Monitor network requests
const responses = [];
page.on('response', response => {
if (response.url().includes('/live-component/')) {
responses.push(response);
}
});
// Click action without fragment specification
await page.click('[data-action="incrementFull"]');
await page.waitForTimeout(100);
// Verify full HTML response (not fragments)
expect(responses.length).toBeGreaterThan(0);
const lastResponse = await responses[responses.length - 1].json();
expect(lastResponse).toHaveProperty('html');
expect(lastResponse.fragments).toBeUndefined();
});
test('should batch multiple fragment updates', async ({ page }) => {
// Monitor network requests
let requestCount = 0;
page.on('request', request => {
if (request.url().includes('/live-component/')) {
requestCount++;
}
});
// Trigger multiple actions rapidly
await page.click('[data-action="increment"]');
await page.click('[data-action="increment"]');
await page.click('[data-action="increment"]');
await page.waitForTimeout(200);
// Verify requests were batched (should be < 3)
expect(requestCount).toBeLessThan(3);
// Verify final state correct (count = 3)
const counterValue = await page.textContent('[data-lc-fragment="counter-value"]');
expect(counterValue).toContain('3');
});
test('should preserve scroll position during fragment update', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/long-list');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Scroll to middle of list
await page.evaluate(() => window.scrollTo(0, 500));
const scrollBefore = await page.evaluate(() => window.scrollY);
// Trigger fragment update (update single list item)
await page.click('[data-action="updateItem"]');
await page.waitForTimeout(100);
// Verify scroll position unchanged
const scrollAfter = await page.evaluate(() => window.scrollY);
expect(scrollAfter).toBe(scrollBefore);
});
test('should handle fragment update errors gracefully', async ({ page }) => {
// Simulate fragment not found scenario
const consoleLogs = [];
page.on('console', msg => {
if (msg.type() === 'warn') {
consoleLogs.push(msg.text());
}
});
// Trigger action that requests non-existent fragment
await page.evaluate(() => {
const component = window.LiveComponents.get('counter:main');
component.call('increment', {}, { fragments: ['#nonexistent'] });
});
await page.waitForTimeout(100);
// Verify warning logged
expect(consoleLogs.some(log => log.includes('Fragment not found'))).toBe(true);
// Verify component still functional (fallback to full render)
const counterValue = await page.textContent('[data-lc-fragment="counter-value"]');
expect(counterValue).toContain('1');
});
test('should update data-attributes in fragments', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/data-attributes');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Get initial data attribute
const initialDataValue = await page.getAttribute('[data-lc-fragment="status"]', 'data-status');
expect(initialDataValue).toBe('pending');
// Trigger status change
await page.click('[data-action="approve"]');
await page.waitForTimeout(100);
// Verify data attribute updated
const updatedDataValue = await page.getAttribute('[data-lc-fragment="status"]', 'data-status');
expect(updatedDataValue).toBe('approved');
});
test('should handle whitespace and formatting changes', async ({ page }) => {
// Trigger update with different whitespace formatting
await page.evaluate(() => {
const component = window.LiveComponents.get('counter:main');
// Server might return formatted HTML with different whitespace
component.call('increment');
});
await page.waitForTimeout(100);
// Verify content updated correctly despite formatting differences
const counterValue = await page.textContent('[data-lc-fragment="counter-value"]');
expect(counterValue.trim()).toBe('1');
});
test('should preserve event listeners on fragments', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/event-listeners');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Attach custom event listener to fragment button
await page.evaluate(() => {
const button = document.querySelector('[data-lc-fragment="action-button"] button');
button.addEventListener('custom-click', () => {
window.customEventTriggered = true;
});
});
// Trigger fragment update (patches button element)
await page.click('[data-action="updateFragment"]');
await page.waitForTimeout(100);
// Trigger custom event
await page.evaluate(() => {
const button = document.querySelector('[data-lc-fragment="action-button"] button');
button.dispatchEvent(new Event('custom-click'));
});
// Verify custom event still works (listener preserved)
const eventTriggered = await page.evaluate(() => window.customEventTriggered);
expect(eventTriggered).toBe(true);
});
});
test.describe('Fragment Rendering Performance', () => {
test('should render fragments faster than full HTML', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/performance');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Measure full render time
const fullRenderStart = Date.now();
await page.evaluate(() => {
window.LiveComponents.get('product-list:main').call('refresh');
});
await page.waitForTimeout(100);
const fullRenderTime = Date.now() - fullRenderStart;
// Measure fragment render time
const fragmentRenderStart = Date.now();
await page.evaluate(() => {
window.LiveComponents.get('product-list:main').call('refresh', {}, {
fragments: ['#product-list-items']
});
});
await page.waitForTimeout(100);
const fragmentRenderTime = Date.now() - fragmentRenderStart;
// Fragment render should be faster
expect(fragmentRenderTime).toBeLessThan(fullRenderTime);
});
test('should reduce network payload with fragments', async ({ page }) => {
const responses = [];
page.on('response', async response => {
if (response.url().includes('/live-component/')) {
const body = await response.text();
responses.push({
type: response.url().includes('fragments') ? 'fragment' : 'full',
size: body.length
});
}
});
// Full render
await page.evaluate(() => {
window.LiveComponents.get('counter:main').call('increment');
});
await page.waitForTimeout(100);
// Fragment render
await page.evaluate(() => {
window.LiveComponents.get('counter:main').call('increment', {}, {
fragments: ['#counter-value']
});
});
await page.waitForTimeout(100);
// Compare payload sizes
const fullResponse = responses.find(r => r.type === 'full');
const fragmentResponse = responses.find(r => r.type === 'fragment');
expect(fragmentResponse.size).toBeLessThan(fullResponse.size);
});
});
test.describe('Fragment Rendering Edge Cases', () => {
test('should handle empty fragments', async ({ page }) => {
await page.evaluate(() => {
const component = window.LiveComponents.get('counter:main');
component.call('clear');
});
await page.waitForTimeout(100);
// Verify empty fragment rendered
const fragmentContent = await page.textContent('[data-lc-fragment="counter-value"]');
expect(fragmentContent.trim()).toBe('');
});
test('should handle very large fragments', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/large-fragment');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Trigger update of large fragment (1000+ items)
const startTime = Date.now();
await page.click('[data-action="loadMore"]');
await page.waitForTimeout(500);
const duration = Date.now() - startTime;
// Should complete in reasonable time (<1 second)
expect(duration).toBeLessThan(1000);
// Verify items rendered
const itemCount = await page.$$eval('[data-lc-fragment="items-list"] .item', items => items.length);
expect(itemCount).toBeGreaterThan(100);
});
test('should handle rapid successive fragment updates', async ({ page }) => {
// Trigger 10 updates rapidly
for (let i = 0; i < 10; i++) {
await page.click('[data-action="increment"]');
}
await page.waitForTimeout(200);
// Verify final state correct
const counterValue = await page.textContent('[data-lc-fragment="counter-value"]');
expect(parseInt(counterValue)).toBe(10);
});
test('should handle fragments with special characters', async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/special-chars');
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Update fragment with special characters (<, >, &, etc.)
await page.click('[data-action="updateSpecialChars"]');
await page.waitForTimeout(100);
// Verify special characters properly escaped
const fragmentText = await page.textContent('[data-lc-fragment="content"]');
expect(fragmentText).toContain('<script>');
expect(fragmentText).toContain('&');
});
});