Test Component
HTML; expect($html)->toContain('data-env="development"'); expect($html)->toContain('data-component-id'); }); it('does not initialize devtools in production mode', function () { $html = <<<'HTML'
Test Component
HTML; expect($html)->toContain('data-env="production"'); }); it('tracks component initialization', function () { // Simulate component registration event $event = [ 'type' => 'component:initialized', 'detail' => [ 'componentId' => 'comp-abc-123', 'componentName' => 'UserProfile', 'initialState' => ['userId' => 1, 'name' => 'John Doe'] ] ]; expect($event['type'])->toBe('component:initialized'); expect($event['detail'])->toHaveKey('componentId'); expect($event['detail'])->toHaveKey('componentName'); expect($event['detail'])->toHaveKey('initialState'); }); it('tracks component actions with timing', function () { $actionEvent = [ 'type' => 'component:action', 'detail' => [ 'componentId' => 'comp-abc-123', 'actionName' => 'handleClick', 'startTime' => 1000.5, 'endTime' => 1025.8, 'duration' => 25.3, 'success' => true ] ]; expect($actionEvent['detail']['duration'])->toBeGreaterThan(0); expect($actionEvent['detail']['success'])->toBeTrue(); }); it('tracks network requests', function () { $networkEvent = [ 'type' => 'component:network', 'detail' => [ 'componentId' => 'comp-abc-123', 'method' => 'POST', 'url' => '/api/users', 'status' => 200, 'duration' => 145.5, 'requestBody' => ['name' => 'John'], 'responseBody' => ['id' => 1, 'name' => 'John'] ] ]; expect($networkEvent['detail']['status'])->toBe(200); expect($networkEvent['detail']['duration'])->toBeGreaterThan(0); }); it('tracks component state changes', function () { $stateEvent = [ 'type' => 'component:state-changed', 'detail' => [ 'componentId' => 'comp-abc-123', 'previousState' => ['count' => 0], 'newState' => ['count' => 1], 'timestamp' => time() ] ]; expect($stateEvent['detail']['newState']['count'])->toBe(1); expect($stateEvent['detail']['previousState']['count'])->toBe(0); }); it('tracks component destruction', function () { $destroyEvent = [ 'type' => 'component:destroyed', 'detail' => [ 'componentId' => 'comp-abc-123', 'componentName' => 'UserProfile', 'timestamp' => time() ] ]; expect($destroyEvent['type'])->toBe('component:destroyed'); expect($destroyEvent['detail'])->toHaveKey('componentId'); }); it('filters action log by component', function () { $actionLog = [ ['componentId' => 'comp-1', 'actionName' => 'handleClick'], ['componentId' => 'comp-2', 'actionName' => 'handleSubmit'], ['componentId' => 'comp-1', 'actionName' => 'handleChange'], ['componentId' => 'comp-3', 'actionName' => 'handleClick'], ]; $filtered = array_filter($actionLog, fn($log) => $log['componentId'] === 'comp-1'); expect(count($filtered))->toBe(2); }); it('filters action log by action name', function () { $actionLog = [ ['componentId' => 'comp-1', 'actionName' => 'handleClick'], ['componentId' => 'comp-2', 'actionName' => 'handleSubmit'], ['componentId' => 'comp-3', 'actionName' => 'handleClick'], ]; $filtered = array_filter($actionLog, fn($log) => $log['actionName'] === 'handleClick'); expect(count($filtered))->toBe(2); }); it('clears action log', function () { $actionLog = [ ['componentId' => 'comp-1', 'actionName' => 'handleClick'], ['componentId' => 'comp-2', 'actionName' => 'handleSubmit'], ]; expect(count($actionLog))->toBe(2); $actionLog = []; expect(count($actionLog))->toBe(0); }); it('exports action log as JSON', function () { $actionLog = [ ['componentId' => 'comp-1', 'actionName' => 'handleClick', 'duration' => 25.5], ['componentId' => 'comp-2', 'actionName' => 'handleSubmit', 'duration' => 35.8], ]; $json = json_encode($actionLog, JSON_PRETTY_PRINT); expect($json)->toBeString(); expect($json)->toContain('handleClick'); expect($json)->toContain('handleSubmit'); }); it('creates DOM badge for component', function () { $badge = [ 'componentId' => 'comp-abc-123', 'componentName' => 'UserProfile', 'actionCount' => 5, 'position' => ['top' => 100, 'left' => 50], ]; expect($badge['componentId'])->toBe('comp-abc-123'); expect($badge['actionCount'])->toBe(5); expect($badge['position'])->toHaveKey('top'); expect($badge['position'])->toHaveKey('left'); }); it('updates badge action count', function () { $badge = ['actionCount' => 5]; $badge['actionCount']++; expect($badge['actionCount'])->toBe(6); }); it('removes badge when component destroyed', function () { $badges = [ 'comp-1' => ['componentId' => 'comp-1', 'actionCount' => 5], 'comp-2' => ['componentId' => 'comp-2', 'actionCount' => 3], ]; unset($badges['comp-1']); expect(count($badges))->toBe(1); expect($badges)->not->toHaveKey('comp-1'); expect($badges)->toHaveKey('comp-2'); }); it('records performance metrics during recording', function () { $performanceRecording = []; // Simulate recording start $isRecording = true; if ($isRecording) { $performanceRecording[] = [ 'type' => 'action', 'componentId' => 'comp-1', 'actionName' => 'handleClick', 'duration' => 25.5, 'startTime' => 1000.0, 'endTime' => 1025.5, 'timestamp' => time() ]; } expect(count($performanceRecording))->toBe(1); expect($performanceRecording[0]['type'])->toBe('action'); }); it('does not record performance when not recording', function () { $performanceRecording = []; // Simulate recording stopped $isRecording = false; if ($isRecording) { $performanceRecording[] = [ 'type' => 'action', 'componentId' => 'comp-1', 'actionName' => 'handleClick', 'duration' => 25.5, ]; } expect(count($performanceRecording))->toBe(0); }); it('takes memory snapshots during recording', function () { $memorySnapshots = []; // Simulate memory snapshot $memorySnapshots[] = [ 'timestamp' => time(), 'usedJSHeapSize' => 25000000, 'totalJSHeapSize' => 50000000, 'jsHeapSizeLimit' => 2000000000, ]; expect(count($memorySnapshots))->toBe(1); expect($memorySnapshots[0])->toHaveKey('usedJSHeapSize'); expect($memorySnapshots[0])->toHaveKey('totalJSHeapSize'); }); it('calculates memory delta correctly', function () { $initialMemory = 25000000; $currentMemory = 28000000; $delta = $currentMemory - $initialMemory; expect($delta)->toBe(3000000); expect($delta)->toBeGreaterThan(0); // Memory increased }); it('aggregates action execution times', function () { $actionExecutionTimes = []; // Record multiple executions of same action $key = 'comp-1:handleClick'; $actionExecutionTimes[$key] = [25.5, 30.2, 28.8, 32.1]; $totalTime = array_sum($actionExecutionTimes[$key]); $avgTime = $totalTime / count($actionExecutionTimes[$key]); $count = count($actionExecutionTimes[$key]); expect($totalTime)->toBeGreaterThan(100); expect($avgTime)->toBeGreaterThan(25); expect($count)->toBe(4); }); it('aggregates component render times', function () { $componentRenderTimes = []; $componentRenderTimes['comp-1'] = [45.5, 40.2, 42.8]; $totalTime = array_sum($componentRenderTimes['comp-1']); $avgTime = $totalTime / count($componentRenderTimes['comp-1']); expect($totalTime)->toBeGreaterThan(120); expect($avgTime)->toBeGreaterThan(40); }); it('limits performance recording to last 100 entries', function () { $performanceRecording = []; // Add 150 entries for ($i = 0; $i < 150; $i++) { $performanceRecording[] = [ 'type' => 'action', 'componentId' => "comp-{$i}", 'duration' => 25.5, ]; // Keep only last 100 if (count($performanceRecording) > 100) { array_shift($performanceRecording); } } expect(count($performanceRecording))->toBe(100); }); it('limits memory snapshots to last 100', function () { $memorySnapshots = []; // Add 150 snapshots for ($i = 0; $i < 150; $i++) { $memorySnapshots[] = [ 'timestamp' => time() + $i, 'usedJSHeapSize' => 25000000 + ($i * 10000), ]; // Keep only last 100 if (count($memorySnapshots) > 100) { array_shift($memorySnapshots); } } expect(count($memorySnapshots))->toBe(100); }); it('formats bytes correctly', function () { $formatBytes = function (int $bytes): string { if ($bytes === 0) return '0 B'; $k = 1024; $sizes = ['B', 'KB', 'MB', 'GB']; $i = (int) floor(log($bytes) / log($k)); return round($bytes / pow($k, $i), 2) . ' ' . $sizes[$i]; }; expect($formatBytes(0))->toBe('0 B'); expect($formatBytes(1024))->toBe('1 KB'); expect($formatBytes(1048576))->toBe('1 MB'); expect($formatBytes(1073741824))->toBe('1 GB'); expect($formatBytes(2621440))->toBe('2.5 MB'); }); it('calculates performance summary correctly', function () { $performanceRecording = [ ['type' => 'action', 'duration' => 25.5], ['type' => 'action', 'duration' => 30.2], ['type' => 'render', 'duration' => 45.5], ['type' => 'render', 'duration' => 40.2], ]; $actionCount = count(array_filter($performanceRecording, fn($r) => $r['type'] === 'action')); $renderCount = count(array_filter($performanceRecording, fn($r) => $r['type'] === 'render')); $totalEvents = count($performanceRecording); $actionDurations = array_map( fn($r) => $r['duration'], array_filter($performanceRecording, fn($r) => $r['type'] === 'action') ); $avgActionTime = $actionCount > 0 ? array_sum($actionDurations) / $actionCount : 0; $totalDuration = array_sum(array_column($performanceRecording, 'duration')); expect($totalEvents)->toBe(4); expect($actionCount)->toBe(2); expect($renderCount)->toBe(2); expect($avgActionTime)->toBeGreaterThan(27); expect($totalDuration)->toBeGreaterThan(140); }); it('toggles devtools visibility with keyboard shortcut', function () { // Simulate Ctrl+Shift+D press $event = [ 'key' => 'D', 'ctrlKey' => true, 'shiftKey' => true, 'altKey' => false, ]; $shouldToggle = $event['key'] === 'D' && $event['ctrlKey'] && $event['shiftKey']; expect($shouldToggle)->toBeTrue(); }); it('switches between tabs correctly', function () { $tabs = ['components', 'actions', 'events', 'network', 'performance']; $activeTab = 'components'; $activeTab = 'performance'; expect($activeTab)->toBe('performance'); expect(in_array($activeTab, $tabs))->toBeTrue(); }); it('exports network log correctly', function () { $networkLog = [ [ 'componentId' => 'comp-1', 'method' => 'POST', 'url' => '/api/users', 'status' => 200, 'duration' => 145.5, ], [ 'componentId' => 'comp-2', 'method' => 'GET', 'url' => '/api/users/1', 'status' => 200, 'duration' => 85.2, ], ]; $json = json_encode($networkLog, JSON_PRETTY_PRINT); expect($json)->toBeString(); expect($json)->toContain('POST'); expect($json)->toContain('GET'); expect($json)->toContain('/api/users'); }); });