/** * 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); }); }); });