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,865 @@
/**
* LiveComponentDevTools Integration Tests
*
* Comprehensive tests for the DevTools module including:
* - Component discovery and inspection
* - Action/event logging functionality
* - Network monitoring (SSE, Batch, Upload)
* - DOM badge functionality
* - Performance metrics collection
*
* @module Tests/JavaScript/LiveComponentDevTools
*/
import { beforeEach, afterEach, describe, it, expect, jest } from '@jest/globals';
import { LiveComponentDevTools } from '../../resources/js/modules/LiveComponentDevTools.js';
import { Core } from '../../resources/js/core.js';
describe('LiveComponentDevTools', () => {
let devTools;
let mockElement;
beforeEach(() => {
// Clean up DOM
document.body.innerHTML = '';
// Mock environment to enable DevTools
document.documentElement.dataset.env = 'development';
// Clear localStorage
localStorage.clear();
// Create mock component element
mockElement = document.createElement('div');
mockElement.dataset.componentId = 'test-component-123';
mockElement.dataset.componentName = 'TestComponent';
mockElement.dataset.testProp = 'test-value';
document.body.appendChild(mockElement);
// Initialize DevTools
devTools = new LiveComponentDevTools();
});
afterEach(() => {
// Clean up
if (devTools?.removeAllBadges) {
devTools.removeAllBadges();
}
document.body.innerHTML = '';
delete window.__liveComponentDevTools;
});
describe('Initialization', () => {
it('should initialize DevTools in development mode', () => {
expect(devTools).toBeDefined();
expect(devTools.isEnabled).toBe(true);
expect(devTools.overlay).not.toBeNull();
});
it('should not initialize in production mode', () => {
document.documentElement.dataset.env = 'production';
localStorage.removeItem('livecomponent_devtools');
const prodDevTools = new LiveComponentDevTools();
expect(prodDevTools.isEnabled).toBe(false);
expect(prodDevTools.overlay).toBeNull();
});
it('should initialize with explicit localStorage activation', () => {
document.documentElement.dataset.env = 'production';
localStorage.setItem('livecomponent_devtools', 'true');
const explicitDevTools = new LiveComponentDevTools();
expect(explicitDevTools.isEnabled).toBe(true);
expect(explicitDevTools.overlay).not.toBeNull();
explicitDevTools.removeAllBadges();
});
it('should create overlay DOM structure', () => {
const overlay = document.getElementById('livecomponent-devtools');
expect(overlay).not.toBeNull();
expect(overlay.classList.contains('lc-devtools')).toBe(true);
});
it('should have all expected tabs', () => {
const tabs = devTools.overlay.querySelectorAll('[data-tab]');
expect(tabs.length).toBe(5);
expect(Array.from(tabs).map(t => t.dataset.tab)).toEqual([
'components',
'actions',
'events',
'performance',
'network'
]);
});
it('should register global keyboard shortcuts', (done) => {
// Spy on toggle method
const toggleSpy = jest.spyOn(devTools, 'toggle');
// Simulate Ctrl+Shift+D
const event = new KeyboardEvent('keydown', {
key: 'D',
ctrlKey: true,
shiftKey: true,
bubbles: true
});
document.dispatchEvent(event);
setTimeout(() => {
expect(toggleSpy).toHaveBeenCalled();
toggleSpy.mockRestore();
done();
}, 100);
});
});
describe('Component Discovery and Inspection', () => {
it('should discover components in DOM', () => {
devTools.refreshComponents();
expect(devTools.components.size).toBe(1);
expect(devTools.components.has('test-component-123')).toBe(true);
});
it('should extract component metadata', () => {
devTools.refreshComponents();
const component = devTools.components.get('test-component-123');
expect(component).toEqual({
id: 'test-component-123',
name: 'TestComponent',
element: mockElement,
state: {},
props: {
testProp: 'test-value'
}
});
});
it('should discover multiple components', () => {
const component2 = document.createElement('div');
component2.dataset.componentId = 'component-456';
component2.dataset.componentName = 'AnotherComponent';
document.body.appendChild(component2);
devTools.refreshComponents();
expect(devTools.components.size).toBe(2);
expect(devTools.components.has('component-456')).toBe(true);
});
it('should extract component state from custom property', () => {
mockElement.__liveComponentState = {
count: 5,
active: true
};
devTools.refreshComponents();
const component = devTools.components.get('test-component-123');
expect(component.state).toEqual({
count: 5,
active: true
});
});
it('should render components tree', () => {
devTools.refreshComponents();
devTools.switchTab('components');
const container = devTools.overlay.querySelector('[data-content="components"]');
expect(container.innerHTML).toContain('TestComponent');
expect(container.innerHTML).toContain('#test-component-123');
});
it('should show empty state when no components', () => {
document.body.innerHTML = '';
devTools.refreshComponents();
devTools.switchTab('components');
const container = devTools.overlay.querySelector('[data-content="components"]');
expect(container.innerHTML).toContain('No LiveComponents found');
});
it('should expand component details on click', (done) => {
devTools.refreshComponents();
devTools.switchTab('components');
const componentItem = devTools.overlay.querySelector('[data-component-id="test-component-123"]');
const details = componentItem.querySelector('.lc-component-details');
expect(details.style.display).toBe('none');
componentItem.click();
setTimeout(() => {
expect(details.style.display).toBe('block');
done();
}, 50);
});
});
describe('Action Logging', () => {
it('should log action execution', () => {
const startTime = performance.now();
const endTime = startTime + 25.5;
devTools.logAction(
'test-component-123',
'handleSubmit',
{ formData: 'test' },
startTime,
endTime,
true
);
expect(devTools.actionLog.length).toBe(1);
expect(devTools.actionLog[0]).toMatchObject({
componentId: 'test-component-123',
actionName: 'handleSubmit',
params: { formData: 'test' },
duration: 25.5,
success: true,
error: null
});
});
it('should log action errors', () => {
const startTime = performance.now();
const endTime = startTime + 10;
devTools.logAction(
'test-component-123',
'handleSubmit',
{},
startTime,
endTime,
false,
'Validation failed'
);
const action = devTools.actionLog[0];
expect(action.success).toBe(false);
expect(action.error).toBe('Validation failed');
});
it('should limit action log to 100 entries', () => {
const startTime = performance.now();
// Log 150 actions
for (let i = 0; i < 150; i++) {
devTools.logAction(
'test-component-123',
`action${i}`,
{},
startTime,
startTime + 10,
true
);
}
expect(devTools.actionLog.length).toBe(100);
});
it('should render action log', () => {
devTools.logAction(
'test-component-123',
'handleClick',
{ x: 100, y: 200 },
performance.now(),
performance.now() + 15,
true
);
devTools.switchTab('actions');
const container = devTools.overlay.querySelector('[data-content="actions"]');
expect(container.innerHTML).toContain('test-component-123');
expect(container.innerHTML).toContain('handleClick()');
});
it('should clear action log', () => {
devTools.logAction(
'test-component-123',
'test',
{},
performance.now(),
performance.now() + 10,
true
);
expect(devTools.actionLog.length).toBe(1);
devTools.clearActionLog();
expect(devTools.actionLog.length).toBe(0);
});
it('should track action execution times during recording', () => {
devTools.isRecording = true;
const startTime = performance.now();
const endTime = startTime + 25;
devTools.logAction(
'test-component-123',
'testAction',
{},
startTime,
endTime,
true
);
const key = 'test-component-123:testAction';
expect(devTools.actionExecutionTimes.has(key)).toBe(true);
expect(devTools.actionExecutionTimes.get(key)).toEqual([25]);
});
});
describe('Event Logging', () => {
it('should log events', () => {
devTools.logEvent('user:login', { userId: 123 }, 'client');
expect(devTools.eventLog.length).toBe(1);
expect(devTools.eventLog[0]).toMatchObject({
eventName: 'user:login',
data: { userId: 123 },
source: 'client'
});
});
it('should log server events differently', () => {
devTools.logEvent('sse:message', { count: 5 }, 'server');
const event = devTools.eventLog[0];
expect(event.source).toBe('server');
});
it('should limit event log to 100 entries', () => {
// Log 150 events
for (let i = 0; i < 150; i++) {
devTools.logEvent(`event${i}`, {}, 'client');
}
expect(devTools.eventLog.length).toBe(100);
});
it('should render event log', () => {
devTools.logEvent('test:event', { data: 'test' }, 'client');
devTools.switchTab('events');
const container = devTools.overlay.querySelector('[data-content="events"]');
expect(container.innerHTML).toContain('test:event');
expect(container.innerHTML).toContain('[client]');
});
it('should clear event log', () => {
devTools.logEvent('test', {}, 'client');
expect(devTools.eventLog.length).toBe(1);
devTools.clearEventLog();
expect(devTools.eventLog.length).toBe(0);
});
it('should intercept Core.emit calls', () => {
const originalEmit = Core.emit;
// Emit event through Core
Core.emit('test:event', { data: 'intercepted' });
// Should be logged
const loggedEvent = devTools.eventLog.find(e => e.eventName === 'test:event');
expect(loggedEvent).toBeDefined();
expect(loggedEvent.data).toEqual({ data: 'intercepted' });
expect(loggedEvent.source).toBe('client');
});
});
describe('Performance Profiling', () => {
it('should start performance recording', () => {
devTools.togglePerformanceRecording();
expect(devTools.isRecording).toBe(true);
expect(devTools.performanceRecording).toEqual([]);
});
it('should stop performance recording', () => {
devTools.isRecording = true;
devTools.togglePerformanceRecording();
expect(devTools.isRecording).toBe(false);
});
it('should record action execution during recording', () => {
devTools.isRecording = true;
devTools.recordActionExecution(
'test-component',
'testAction',
25.5,
performance.now(),
performance.now() + 25.5
);
expect(devTools.performanceRecording.length).toBe(1);
expect(devTools.performanceRecording[0]).toMatchObject({
type: 'action',
componentId: 'test-component',
actionName: 'testAction',
duration: 25.5
});
});
it('should record component renders during recording', () => {
devTools.isRecording = true;
devTools.recordComponentRender(
'test-component',
15.2,
performance.now(),
performance.now() + 15.2
);
expect(devTools.performanceRecording.length).toBe(1);
expect(devTools.performanceRecording[0].type).toBe('render');
});
it('should track component render times', () => {
devTools.isRecording = true;
devTools.recordComponentRender('comp-1', 10, 0, 10);
devTools.recordComponentRender('comp-1', 15, 10, 25);
devTools.recordComponentRender('comp-1', 12, 25, 37);
expect(devTools.componentRenderTimes.get('comp-1')).toEqual([10, 15, 12]);
});
it('should take memory snapshots if available', () => {
if (!performance.memory) {
// Skip if not available (not in Chrome)
return;
}
devTools.takeMemorySnapshot();
expect(devTools.memorySnapshots.length).toBe(1);
expect(devTools.memorySnapshots[0]).toHaveProperty('usedJSHeapSize');
expect(devTools.memorySnapshots[0]).toHaveProperty('totalJSHeapSize');
});
it('should clear performance data', () => {
devTools.performanceRecording = [{ type: 'action', duration: 10 }];
devTools.memorySnapshots = [{ timestamp: Date.now() }];
devTools.clearPerformanceData();
expect(devTools.performanceRecording).toEqual([]);
expect(devTools.memorySnapshots).toEqual([]);
expect(devTools.componentRenderTimes.size).toBe(0);
expect(devTools.actionExecutionTimes.size).toBe(0);
});
it('should render performance summary', () => {
devTools.isRecording = true;
// Add test data
devTools.recordActionExecution('comp-1', 'action1', 20, 0, 20);
devTools.recordActionExecution('comp-1', 'action2', 30, 20, 50);
devTools.recordComponentRender('comp-1', 15, 50, 65);
devTools.togglePerformanceRecording(); // Stop and render
const container = devTools.overlay.querySelector('[data-content="performance"]');
const html = container.innerHTML;
expect(html).toContain('Performance Summary');
expect(html).toContain('Total Events');
});
it('should format bytes correctly', () => {
expect(devTools.formatBytes(0)).toBe('0 B');
expect(devTools.formatBytes(1024)).toBe('1 KB');
expect(devTools.formatBytes(1048576)).toBe('1 MB');
expect(devTools.formatBytes(1073741824)).toBe('1 GB');
});
});
describe('Network Monitoring', () => {
it('should monitor fetch requests', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: async () => ({ data: 'test' })
})
);
await fetch('/api/test', { method: 'POST' });
// Give network log time to update
await new Promise(resolve => setTimeout(resolve, 50));
expect(devTools.networkLog.length).toBeGreaterThan(0);
const request = devTools.networkLog[0];
expect(request.method).toBe('POST');
expect(request.url).toBe('/api/test');
expect(request.status).toBe(200);
expect(request.duration).toBeGreaterThan(0);
});
it('should log fetch errors', async () => {
global.fetch = jest.fn(() => Promise.reject(new Error('Network error')));
try {
await fetch('/api/error');
} catch (e) {
// Expected
}
await new Promise(resolve => setTimeout(resolve, 50));
const request = devTools.networkLog[0];
expect(request.status).toBe('Error');
});
it('should limit network log to 50 entries', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200
})
);
// Make 60 requests
for (let i = 0; i < 60; i++) {
await fetch(`/api/test${i}`);
}
await new Promise(resolve => setTimeout(resolve, 100));
expect(devTools.networkLog.length).toBeLessThanOrEqual(50);
});
it('should render network log', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200
})
);
await fetch('/api/test', { method: 'GET' });
await new Promise(resolve => setTimeout(resolve, 50));
devTools.switchTab('network');
const container = devTools.overlay.querySelector('[data-content="network"]');
expect(container.innerHTML).toContain('GET');
expect(container.innerHTML).toContain('/api/test');
});
it('should clear network log', () => {
devTools.networkLog = [
{ timestamp: Date.now(), method: 'GET', url: '/api/test', status: 200, duration: 50 }
];
devTools.clearNetworkLog();
expect(devTools.networkLog.length).toBe(0);
});
});
describe('DOM Badges', () => {
it('should create DOM badges for components', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
expect(devTools.domBadges.size).toBe(1);
expect(devTools.domBadges.has('test-component-123')).toBe(true);
const badge = devTools.domBadges.get('test-component-123').badge;
expect(badge).toBeDefined();
expect(badge.classList.contains('lc-dom-badge')).toBe(true);
done();
}, 100);
});
it('should update badge activity counter', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
devTools.updateBadgeActivity('test-component-123');
const badgeData = devTools.domBadges.get('test-component-123');
expect(badgeData.actionCount).toBe(1);
const actionsSpan = badgeData.badge.querySelector('.lc-badge-actions');
expect(actionsSpan.textContent).toContain('1 action');
done();
}, 100);
});
it('should flash badge on activity', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
const badgeData = devTools.domBadges.get('test-component-123');
devTools.updateBadgeActivity('test-component-123');
expect(badgeData.badge.classList.contains('lc-badge-active')).toBe(true);
setTimeout(() => {
expect(badgeData.badge.classList.contains('lc-badge-active')).toBe(false);
done();
}, 600);
}, 100);
});
it('should toggle badge visibility', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
expect(devTools.badgesEnabled).toBe(true);
devTools.toggleBadges();
expect(devTools.badgesEnabled).toBe(false);
const badgeData = devTools.domBadges.get('test-component-123');
expect(badgeData.badge.style.display).toBe('none');
devTools.toggleBadges();
expect(devTools.badgesEnabled).toBe(true);
expect(badgeData.badge.style.display).toBe('block');
done();
}, 100);
});
it('should clean up badges for removed components', (done) => {
const component2 = document.createElement('div');
component2.dataset.componentId = 'component-456';
component2.dataset.componentName = 'TempComponent';
document.body.appendChild(component2);
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
expect(devTools.domBadges.size).toBe(2);
// Remove component from DOM
component2.remove();
devTools.cleanupRemovedBadges();
expect(devTools.domBadges.size).toBe(1);
expect(devTools.domBadges.has('component-456')).toBe(false);
done();
}, 100);
});
it('should open DevTools on badge click', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
const badgeData = devTools.domBadges.get('test-component-123');
const openSpy = jest.spyOn(devTools, 'open');
badgeData.badge.click();
setTimeout(() => {
expect(openSpy).toHaveBeenCalled();
openSpy.mockRestore();
done();
}, 100);
}, 100);
});
});
describe('UI Interactions', () => {
it('should open and close DevTools', () => {
expect(devTools.isOpen).toBe(false);
devTools.open();
expect(devTools.isOpen).toBe(true);
expect(devTools.overlay.style.display).toBe('block');
devTools.close();
expect(devTools.isOpen).toBe(false);
expect(devTools.overlay.style.display).toBe('none');
});
it('should toggle DevTools', () => {
devTools.toggle();
expect(devTools.isOpen).toBe(true);
devTools.toggle();
expect(devTools.isOpen).toBe(false);
});
it('should switch tabs', () => {
devTools.switchTab('performance');
expect(devTools.activeTab).toBe('performance');
const activeTab = devTools.overlay.querySelector('.lc-devtools__tab--active');
const activePane = devTools.overlay.querySelector('.lc-devtools__pane--active');
expect(activeTab.dataset.tab).toBe('performance');
expect(activePane.dataset.pane).toBe('performance');
});
it('should minimize and restore', () => {
devTools.toggleMinimize();
expect(devTools.overlay.classList.contains('lc-devtools--minimized')).toBe(true);
devTools.toggleMinimize();
expect(devTools.overlay.classList.contains('lc-devtools--minimized')).toBe(false);
});
it('should emit events on open/close', (done) => {
const openHandler = jest.fn();
const closeHandler = jest.fn();
Core.on('devtools:opened', openHandler);
Core.on('devtools:closed', closeHandler);
devTools.open();
setTimeout(() => {
expect(openHandler).toHaveBeenCalled();
devTools.close();
setTimeout(() => {
expect(closeHandler).toHaveBeenCalled();
done();
}, 50);
}, 50);
});
});
describe('Integration with LiveComponent Manager', () => {
it('should connect to LiveComponent manager', (done) => {
// Mock LiveComponent manager
window.LiveComponent = {
enableDevTools: jest.fn()
};
// Wait for connection attempt
setTimeout(() => {
expect(window.LiveComponent.enableDevTools).toHaveBeenCalledWith(devTools);
delete window.LiveComponent;
done();
}, 200);
});
it('should retry connection if LiveComponent not available', (done) => {
const consoleSpy = jest.spyOn(console, 'log');
// LiveComponent not available initially
delete window.LiveComponent;
// Create new DevTools instance
const newDevTools = new LiveComponentDevTools();
setTimeout(() => {
// Make LiveComponent available
window.LiveComponent = {
enableDevTools: jest.fn()
};
setTimeout(() => {
expect(window.LiveComponent.enableDevTools).toHaveBeenCalled();
consoleSpy.mockRestore();
newDevTools.removeAllBadges();
delete window.LiveComponent;
done();
}, 200);
}, 150);
});
});
describe('Responsiveness and Performance', () => {
it('should update badge positions on window scroll', (done) => {
devTools.refreshComponents();
devTools.updateDomBadges();
setTimeout(() => {
const badgeData = devTools.domBadges.get('test-component-123');
const initialTop = badgeData.badge.style.top;
// Mock element position change
mockElement.style.marginTop = '500px';
devTools.updateBadgePosition(badgeData, mockElement);
expect(badgeData.badge.style.top).not.toBe(initialTop);
done();
}, 100);
});
it('should handle rapid action logging without performance degradation', () => {
const startTime = performance.now();
// Log 1000 actions rapidly
for (let i = 0; i < 1000; i++) {
devTools.logAction(
'test-component',
`action${i}`,
{},
performance.now(),
performance.now() + 10,
true
);
}
const duration = performance.now() - startTime;
// Should complete within 500ms
expect(duration).toBeLessThan(500);
// Should cap at 100 entries
expect(devTools.actionLog.length).toBe(100);
});
});
});