- 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.
866 lines
28 KiB
JavaScript
866 lines
28 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|