diff --git a/src/visualizer/ui/ComponentTreeView.js b/src/visualizer/ui/ComponentTreeView.js new file mode 100644 index 0000000..f214b1b --- /dev/null +++ b/src/visualizer/ui/ComponentTreeView.js @@ -0,0 +1,200 @@ +/** + * ComponentTreeView - File/Folder-style component hierarchy tree + * + * Displays Vue components in a file explorer-like tree structure: + * - 📁 Folder icon for components with children (expandable) + * - 📄 File icon for leaf components (no children) + * - Expand/collapse functionality with ▶/▼ indicators + * - Visual tree lines showing parent-child relationships + * - Color-coded component names based on performance (green/yellow/orange/red) + * - Badges showing render counts + * - Click to select component and open insights drawer + * + * This is used exclusively in the Split View's center column. + * The tree structure makes it easy to understand component relationships. + */ +export class ComponentTreeView { + constructor(state, options = {}) { + this.state = state + this.onSelect = options.onSelect || (() => {}) + this.container = null + this.treeRoot = null + this.nodeElements = new Map() // Maps node UIDs to DOM elements for quick lookup + this.selectedUid = null + } + + mount(container) { + this.container = container + this.treeRoot = document.createElement('div') + this.treeRoot.className = 'vri-file-tree' + this.container.innerHTML = '' + this.container.appendChild(this.treeRoot) + this.render() + } + + render() { + if (!this.treeRoot) return + + const roots = this.state.getRootNodes() + this.nodeElements.clear() + + if (!roots.length) { + this.treeRoot.innerHTML = ` +
+ No component activity yet. Interact with your app to populate the tree. +
+ ` + return + } + + const fragment = document.createDocumentFragment() + roots.forEach((node, index) => { + const nodeElement = this._renderNode(node, 0, index === roots.length - 1) + fragment.appendChild(nodeElement) + }) + this.treeRoot.innerHTML = '' + this.treeRoot.appendChild(fragment) + this._syncSelection() + } + + setSelectedNode(node) { + this.selectedUid = node ? String(node.uid) : null + this._syncSelection() + } + + revealNode(node) { + if (!node) return + + // Expand parents so the node becomes visible + let current = node.parent + while (current) { + current.expanded = true + current = current.parent + } + node.expanded = true + + this.render() + + requestAnimationFrame(() => { + const element = this.nodeElements.get(String(node.uid)) + if (element) { + element.scrollIntoView({ block: 'center', behavior: 'smooth' }) + element.classList.add('pulse') + setTimeout(() => element.classList.remove('pulse'), 600) + } + }) + } + + /** + * Render a single node in the tree structure + * Creates a file/folder item with: + * - Expand/collapse toggle (▶/▼) for components with children + * - Folder (📁) or file (📄) icon based on whether it has children + * - Component name color-coded by performance + * - Badge showing total render count + * - Visual indentation based on depth + * + * @param {TreeNode} node - The component node to render + * @param {number} depth - Current depth in the tree (for indentation) + * @param {boolean} isLast - Whether this is the last child (for tree line rendering) + * @returns {HTMLElement} The rendered node wrapper element + */ + _renderNode(node, depth, isLast) { + const wrapper = document.createElement('div') + wrapper.className = 'vri-file-item-wrapper' + wrapper.dataset.uid = node.uid + wrapper.style.marginLeft = `${depth * 20}px` // Indent based on depth + + if (node.selected) { + wrapper.classList.add('selected') + this.selectedUid = String(node.uid) + } + + const fileItem = document.createElement('div') + fileItem.className = 'vri-file-item' + if (node.selected) { + fileItem.classList.add('selected') + } + + // Expand/collapse toggle for components with children + if (node.children.length > 0) { + const toggle = document.createElement('div') + toggle.className = `vri-folder-toggle ${node.expanded ? 'expanded' : ''}` + toggle.innerHTML = '▶' // Rotates to ▼ when expanded + toggle.addEventListener('click', e => { + e.stopPropagation() // Prevent triggering parent click + node.expanded = !node.expanded + this.render() // Re-render to show/hide children + }) + fileItem.appendChild(toggle) + } else { + // Spacer for leaf nodes (no toggle needed) + const spacer = document.createElement('div') + spacer.style.width = '20px' + fileItem.appendChild(spacer) + } + + // File/folder icon: 📁 for components with children, 📄 for leaf components + const icon = document.createElement('div') + icon.className = 'vri-file-icon' + icon.innerHTML = node.children.length > 0 ? '📁' : '📄' + fileItem.appendChild(icon) + + // Component name - color-coded by performance (green/yellow/orange/red) + const fileName = document.createElement('div') + fileName.className = 'vri-file-name' + fileName.textContent = node.componentName + fileName.style.color = node.getColor() // Performance-based color + fileItem.appendChild(fileName) + + // Badge showing total render count (warn class if > 25% unnecessary) + const badge = document.createElement('div') + badge.className = `vri-file-badge ${node.getUnnecessaryPercent() > 25 ? 'warn' : ''}` + badge.textContent = `${node.renderAnalysis.totalRenders}` + fileItem.appendChild(badge) + + // Click handler: select component and open insights drawer + fileItem.addEventListener('click', () => { + if (typeof this.onSelect === 'function') { + this.onSelect(node) + } + }) + + wrapper.appendChild(fileItem) + + // Render children if expanded (recursive) + if (node.children.length > 0 && node.expanded) { + const childrenContainer = document.createElement('div') + childrenContainer.className = 'vri-file-children' + + // Recursively render all children + node.children.forEach((child, index) => { + const isChildLast = index === node.children.length - 1 + childrenContainer.appendChild(this._renderNode(child, depth + 1, isChildLast)) + }) + + wrapper.appendChild(childrenContainer) + } + + // Store element reference for quick lookup (used in revealNode) + this.nodeElements.set(String(node.uid), wrapper) + return wrapper + } + + _syncSelection() { + this.nodeElements.forEach((element, uid) => { + const fileItem = element.querySelector('.vri-file-item') + if (this.selectedUid && uid === this.selectedUid) { + element.classList.add('selected') + if (fileItem) { + fileItem.classList.add('selected') + } + } else { + element.classList.remove('selected') + if (fileItem) { + fileItem.classList.remove('selected') + } + } + }) + } +} diff --git a/src/visualizer/ui/InspectorPanel.js b/src/visualizer/ui/InspectorPanel.js index cca7f20..58544f7 100644 --- a/src/visualizer/ui/InspectorPanel.js +++ b/src/visualizer/ui/InspectorPanel.js @@ -5,31 +5,24 @@ export class InspectorPanel { constructor() { this.panel = null this.selectedNode = null + this.layout = 'overlay' // Overlay panel for canvas view } createPanel() { const inspector = document.createElement('div') inspector.id = 'vri-inspector' - inspector.style.cssText = ` - position: absolute; - right: 20px; - top: 60px; - bottom: 20px; - width: 350px; - background: rgba(30, 30, 30, 0.95); - border: 1px solid rgba(66, 184, 131, 0.3); - border-radius: 8px; - padding: 20px; - overflow-y: auto; - backdrop-filter: blur(10px); - display: none; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - ` - this.panel = inspector + this._applyLayoutStyles() + this.panel.style.display = 'none' + return inspector } + setLayout(layout) { + this.layout = layout + this._applyLayoutStyles() + } + showInspector(node) { if (!this.panel) return @@ -138,6 +131,33 @@ export class InspectorPanel { ` } + _applyLayoutStyles() { + if (!this.panel) return + + const isHidden = this.panel.style.display === 'none' + + // InspectorPanel is only used in overlay mode (canvas view) + // Split view uses its own drawer, not this panel + Object.assign(this.panel.style, { + position: 'absolute', + right: '20px', + top: '60px', + bottom: '20px', + width: '350px', + height: 'auto', + background: 'rgba(30, 30, 30, 0.95)', + border: '1px solid rgba(66, 184, 131, 0.3)', + borderRadius: '8px', + padding: '20px', + overflowY: 'auto', + backdropFilter: 'blur(10px)', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)', + transition: 'transform 0.3s ease, opacity 0.3s ease' + }) + + this.panel.style.display = isHidden ? 'none' : 'block' + } + _renderPerformanceMetrics(node, perf) { if (node.renderAnalysis.avgRenderTime <= 0) return '' diff --git a/src/visualizer/ui/NotificationSystem.js b/src/visualizer/ui/NotificationSystem.js index 5d5c1ab..de52cbc 100644 --- a/src/visualizer/ui/NotificationSystem.js +++ b/src/visualizer/ui/NotificationSystem.js @@ -10,21 +10,23 @@ export class NotificationSystem { this.listElement = null this.navigateCallback = null this.selectCallback = null + this.layout = 'overlay' } createPanel() { // Create notification panel const notificationPanel = document.createElement('div') - notificationPanel.style.cssText = ` - background: rgba(30, 30, 30, 0.95); - border: 1px solid rgba(66, 184, 131, 0.3); - border-radius: 8px; - padding: 15px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - backdrop-filter: blur(10px); - will-change: opacity; - transition: opacity 0.3s ease; - ` + Object.assign(notificationPanel.style, { + background: 'rgba(30, 30, 30, 0.95)', + border: '1px solid rgba(66, 184, 131, 0.3)', + borderRadius: '8px', + padding: '15px', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)', + backdropFilter: 'blur(10px)', + willChange: 'opacity', + transition: 'opacity 0.3s ease', + overflowY: 'auto' + }) const notificationTitle = document.createElement('h4') notificationTitle.style.cssText = ` @@ -72,12 +74,17 @@ export class NotificationSystem { this.panel = notificationPanel this.listElement = notificationList + this._applyLayoutStyles() // Clear notifications handler this._setupClearHandler() return notificationPanel } + setLayout(layout) { + this.layout = layout + this._applyLayoutStyles() + } _setupClearHandler() { // Use setTimeout to ensure DOM is ready @@ -300,6 +307,23 @@ export class NotificationSystem { this.updateBadge() } + _applyLayoutStyles() { + if (!this.panel) return + const wasHidden = this.panel.style.display === 'none' + + // NotificationSystem is only used in overlay mode (canvas view) + // Split view uses its own component changes list, not this panel + Object.assign(this.panel.style, { + position: 'absolute', + left: '20px', + top: '80px', + width: '400px', + maxHeight: '300px', + height: 'auto', + display: wasHidden ? 'none' : 'block' + }) + } + getCount() { return this.notifications.length } diff --git a/src/visualizer/visualizer.js b/src/visualizer/visualizer.js index 35812b4..acf81e8 100644 --- a/src/visualizer/visualizer.js +++ b/src/visualizer/visualizer.js @@ -3,6 +3,7 @@ import { TreeNode } from './nodes/TreeNode.js' import { Quadtree } from './spatial/Quadtree.js' import { NotificationSystem } from './ui/NotificationSystem.js' import { InspectorPanel } from './ui/InspectorPanel.js' +import { ComponentTreeView } from './ui/ComponentTreeView.js' import { CanvasRenderer } from './rendering/CanvasRenderer.js' import { TreeLayout } from './layout/TreeLayout.js' import { VisualizerState } from './state/VisualizerState.js' @@ -56,10 +57,10 @@ export function createEnhancedVisualizerV2(profiler) { VRI-Visualizer v${version} -
-
- Components: 0 -
+
+
+ Components: 0 +
Visible: 0
@@ -67,11 +68,15 @@ export function createEnhancedVisualizerV2(profiler) { FPS: 60
- Memory: 0 MB -
+ Memory: 0 MB
-
+
+
+
+ + +
+
+
+
+ Select a component to view insights +
+
+ + ` document.body.appendChild(container) + const header = container.querySelector('.vri-header') + const canvasView = container.querySelector('#vri-canvas-view') + const structuredView = container.querySelector('#vri-structured-view') + const viewButtons = container.querySelectorAll('.vri-view-btn') + const drawer = container.querySelector('#vri-drawer') + const drawerContent = container.querySelector('#vri-drawer-content') + const drawerClose = container.querySelector('#vri-drawer-close') + const treeColumn = container.querySelector('#vri-tree-column') + const changesList = container.querySelector('#vri-changes-list') + // Create notification panel and position it under the header const notificationPanel = notificationSystem.createPanel() - notificationPanel.style.cssText = ` - position: absolute; - left: 20px; - top: 80px; - width: 400px; - max-height: 300px; - overflow-y: auto; - z-index: 10; - ` + notificationPanel.style.zIndex = '10' + notificationSystem.setLayout('overlay') // Insert notification panel after the header - const header = container.querySelector('.vri-header') header.parentNode.insertBefore(notificationPanel, header.nextSibling) // Append inspector panel container.appendChild(inspectorElement) + let currentView = 'canvas' + + function syncViewButtons(targetView) { + viewButtons.forEach(btn => { + if (btn.getAttribute('data-view') === targetView) { + btn.classList.add('active') + } else { + btn.classList.remove('active') + } + }) + } + + function openDrawer() { + if (drawer) { + drawer.classList.add('open') + } + } + + function closeDrawer() { + if (drawer) { + drawer.classList.remove('open') + } + } + + /** + * Switch between Canvas View and Split View + * Ensures complete isolation between views: + * - Split View: Uses left column (changes list), center column (tree), and drawer (insights) + * - Canvas View: Uses notification panel and inspector panel (original behavior) + * Each view manages its own UI elements independently. + */ + function switchView(targetView) { + if (!targetView || targetView === currentView) return + + currentView = targetView + syncViewButtons(targetView) + + if (targetView === 'structured') { + // SPLIT VIEW: Show split layout, hide canvas + canvasView.style.display = 'none' + structuredView.style.display = 'block' + + // Hide notification panel in split view (left column replaces it) + if (notificationPanel && notificationPanel.parentNode) { + notificationPanel.style.display = 'none' + } + + // Hide inspector panel in split view (drawer replaces it) + inspectorPanel.hideInspector() + closeDrawer() + } else { + // CANVAS VIEW: Show canvas, hide split layout + canvasView.style.display = 'block' + structuredView.style.display = 'none' + + // Show notification panel in canvas view (original behavior) + notificationSystem.setLayout('overlay') + if (notificationPanel) { + notificationPanel.style.display = 'block' + if (!notificationPanel.parentNode || notificationPanel.parentNode !== header.parentNode) { + header.parentNode.insertBefore(notificationPanel, header.nextSibling) + } + } + + // Close drawer in canvas view (inspector panel replaces it) + closeDrawer() + + // If there's a selected node, show it in inspector panel (canvas view behavior) + if (state && state.selectedNode) { + inspectorPanel.showInspector(state.selectedNode) + } + } + } + + // Drawer close handler + if (drawerClose) { + drawerClose.addEventListener('click', () => { + closeDrawer() + if (state) { + state.selectNode(null) + } + }) + } + + /** + * Populate drawer with comprehensive component insights + * Uses InspectorPanel._generateHTML() to ensure all insights are displayed: + * This ensures the drawer shows the same comprehensive data as the canvas view's inspector panel. + */ + function updateDrawerContent(node) { + if (!drawerContent) return + + if (!node) { + drawerContent.innerHTML = ` +
+ Select a component to view insights +
+ ` + return + } + + // Extract all necessary data from node's render analysis + const analysis = node.renderAnalysis + const unnecessaryPercent = node.getUnnecessaryPercent() + const perf = analysis.performanceInsights || {} + const patterns = analysis.changePatterns || {} + const renderHistory = analysis.renderHistory || [] + const recentRenders = renderHistory.slice(-10).reverse() + const detailedChanges = analysis.detailedChanges || {} + const eventTracking = analysis.eventTracking || {} + const reactivityTracking = analysis.reactivityTracking || {} + const sourceInfo = analysis.sourceInfo || {} + + // Use InspectorPanel's HTML generation to ensure consistency with canvas view + // This method generates all sections: performance, patterns, changes, reactivity, etc. + const content = inspectorPanel._generateHTML(node, { + unnecessaryPercent, + perf, + patterns, + recentRenders, + detailedChanges, + eventTracking, + reactivityTracking, + sourceInfo + }) + + drawerContent.innerHTML = content + } + + // Helper function to escape HTML + function escapeHtml(text) { + const div = document.createElement('div') + div.textContent = text + return div.innerHTML + } + + viewButtons.forEach(btn => { + btn.addEventListener('click', () => { + const target = btn.getAttribute('data-view') + switchView(target) + }) + }) + // Notification system already handles the clear button internally // Notification functions function addNotification(event) { notificationSystem.addNotification(event) + updateChangesList() + } + + /** + * Update the component changes list in the left column + * Displays the 50 most recent component render events, sorted by timestamp. + * Each item is color-coded based on performance metrics: + * Clicking an item selects the component, reveals it in the tree, and opens the insights drawer. + */ + function updateChangesList() { + if (!changesList || !state) return + + // Get all nodes, sort by most recent update, limit to 50 + const nodes = Array.from(state.nodes.values()) + .sort((a, b) => b.lastUpdateTime - a.lastUpdateTime) + .slice(0, 50) + + if (nodes.length === 0) { + changesList.innerHTML = ` +
+ No component changes yet +
+ ` + return + } + + const fragment = document.createDocumentFragment() + nodes.forEach(node => { + const item = document.createElement('div') + item.className = 'vri-change-item' + item.dataset.uid = node.uid + + // Calculate performance-based color coding + const unnecessaryPercent = node.getUnnecessaryPercent() + const bottleneckScore = node.renderAnalysis.bottleneckScore || 0 + let borderColor = '#42b883' // green (default - good performance) + + // Determine color based on performance metrics (matches canvas view color logic) + if (bottleneckScore > 20 || unnecessaryPercent > 50) { + borderColor = '#f44336' // red - critical issues + } else if (bottleneckScore > 10 || unnecessaryPercent > 30) { + borderColor = '#ff9800' // orange - high issues + } else if (unnecessaryPercent > 10) { + borderColor = '#ffc107' // yellow - moderate issues + } + + item.style.borderColor = borderColor + + // Format relative time (just now, Xs ago, Xm ago) + const timeAgo = Date.now() - node.lastUpdateTime + const timeText = timeAgo < 1000 ? 'just now' : + timeAgo < 60000 ? `${Math.floor(timeAgo / 1000)}s ago` : + `${Math.floor(timeAgo / 60000)}m ago` + + item.innerHTML = ` +
${escapeHtml(node.componentName)}
+
${timeText} • ${node.renderAnalysis.totalRenders} renders
+ ` + + // Click handler: select component, reveal in tree, open drawer + item.addEventListener('click', () => { + selectNode(node) + if (treeView) { + treeView.revealNode(node) // Expand parents and scroll to node + } + }) + + fragment.appendChild(item) + }) + + changesList.innerHTML = '' + changesList.appendChild(fragment) } // Need to define this after state is created let renderNotifications = () => {} let calculateTreeLayout = () => {} + let state = null + let treeView = null function navigateToNode(node) { if (!node) return @@ -229,15 +769,23 @@ export function createEnhancedVisualizerV2(profiler) { } // State management - const state = new VisualizerState() + state = new VisualizerState() + + if (treeColumn) { + treeView = new ComponentTreeView(state, { + onSelect: node => selectNode(node) + }) + treeView.mount(treeColumn) + } - // Initialize canvas renderer + // Initialize canvas renderer for main canvas const canvasRenderer = new CanvasRenderer(container, state) // Set canvas renderer callbacks and initialize canvasRenderer.onNodeClick = node => selectNode(node) canvasRenderer.initialize() + // Initialize tree layout const treeLayout = new TreeLayout({ levelHeight: 180, @@ -260,22 +808,30 @@ export function createEnhancedVisualizerV2(profiler) { } // Set callbacks for navigation and selection after state is created - notificationSystem.setCallbacks( - uid => { - // Navigate callback - const node = state.getNode(uid) - if (node) { - navigateToNode(node) - } - }, - uid => { - // Select callback - const node = state.getNode(uid) - if (node) { - selectNode(node) + function handleNavigateFromNotification(uid) { + const node = state.getNode(uid) + if (!node) return + + if (currentView === 'structured') { + if (treeView) { + treeView.revealNode(node) } + selectNode(node) + } else { + navigateToNode(node) } - ) + } + + function handleSelectFromNotification(uid) { + const node = state.getNode(uid) + if (!node) return + if (currentView === 'structured' && treeView) { + treeView.revealNode(node) + } + selectNode(node) + } + + notificationSystem.setCallbacks(handleNavigateFromNotification, handleSelectFromNotification) // Optimized tree layout calculateTreeLayout = function () { @@ -358,6 +914,13 @@ export function createEnhancedVisualizerV2(profiler) { if (state.shouldPruneMemory()) { state.pruneMemory() } + + if (treeView) { + treeView.render() + } + + // Update changes list + updateChangesList() } // Layout update throttling @@ -406,14 +969,46 @@ export function createEnhancedVisualizerV2(profiler) { state.updateCamera({ x: cameraPos.x, y: cameraPos.y }) } - // Node selection + /** + * Handle node selection - routes to appropriate UI based on current view + * This ensures complete isolation between split view and canvas view: + * + * Split View (structured): + * - Updates drawer content with full insights + * - Opens drawer (slides in from right) + * - Updates tree view selection + * + * Canvas View: + * - Uses original InspectorPanel (overlay panel) + * - Updates tree view selection (if tree view exists) + * + * This separation ensures the two views don't interfere with each other. + */ function selectNode(node) { state.selectNode(node) if (node) { - inspectorPanel.showInspector(node) + if (currentView === 'structured') { + // SPLIT VIEW: Use drawer for insights + updateDrawerContent(node) + openDrawer() + } else { + // CANVAS VIEW: Use inspector panel (original behavior) + inspectorPanel.showInspector(node) + } } else { - inspectorPanel.hideInspector() + // Deselect: clear and close appropriate UI + if (currentView === 'structured') { + updateDrawerContent(null) + closeDrawer() + } else { + inspectorPanel.hideInspector() + } + } + + // Update tree view selection (works in both views) + if (treeView) { + treeView.setSelectedNode(node) } } diff --git a/tests/visualizer/visualizer.test.js b/tests/visualizer/visualizer.test.js index 4cc5239..bae721e 100644 --- a/tests/visualizer/visualizer.test.js +++ b/tests/visualizer/visualizer.test.js @@ -166,6 +166,20 @@ describe('Advanced Optimized Visualizer', () => { expect(inspector.style.position).toBe('absolute') expect(inspector.style.bottom).toBe('20px') }) + + it('should setup split view containers and toggle controls', () => { + createEnhancedVisualizerV2(mockProfiler) + + const structuredView = document.getElementById('vri-structured-view') + const canvasView = document.getElementById('vri-canvas-view') + const viewButtons = document.querySelectorAll('.vri-view-btn') + + expect(structuredView).toBeTruthy() + expect(canvasView).toBeTruthy() + expect(structuredView.style.display).toBe('none') + expect(canvasView.style.display).not.toBe('none') + expect(viewButtons.length).toBeGreaterThanOrEqual(2) + }) }) describe('TreeNode Class', () => { @@ -398,6 +412,67 @@ describe('Advanced Optimized Visualizer', () => { }) }) + describe('Split View Layout', () => { + it('should toggle between canvas and split view layouts', () => { + createEnhancedVisualizerV2(mockProfiler) + + const structuredBtn = document.querySelector('[data-view="structured"]') + const canvasBtn = document.querySelector('[data-view="canvas"]') + const structuredView = document.getElementById('vri-structured-view') + const canvasView = document.getElementById('vri-canvas-view') + const notificationPanel = document.querySelector('[id="notification-list"]').parentElement + + expect(notificationPanel.style.position).toBe('absolute') + + structuredBtn.click() + expect(structuredView.style.display).toBe('block') + expect(canvasView.style.display).toBe('none') + expect(notificationPanel.style.position).toBe('relative') + + canvasBtn.click() + expect(canvasView.style.display).toBe('block') + expect(structuredView.style.display).toBe('none') + expect(notificationPanel.style.position).toBe('absolute') + }) + + it('should render component tree nodes and open drawer on selection', async () => { + const { subscribeToRenderEvents } = await import('../../src/core/broadcast-channel.js') + let capturedCallback + + subscribeToRenderEvents.mockImplementation(callback => { + capturedCallback = callback + return () => {} + }) + + createEnhancedVisualizerV2(mockProfiler) + + capturedCallback({ + uid: 'abc', + componentName: 'TreeComponent', + timestamp: Date.now(), + duration: 4, + isUnnecessary: false + }) + + await vi.advanceTimersByTimeAsync(50) + + const structuredBtn = document.querySelector('[data-view="structured"]') + structuredBtn.click() + + const treeNodes = document.querySelectorAll('.vri-tree-node') + expect(treeNodes.length).toBeGreaterThan(0) + + const header = treeNodes[0].querySelector('.vri-tree-node-header') + header.click() + + const drawer = document.getElementById('vri-drawer') + expect(drawer.classList.contains('open')).toBe(true) + + const inspector = document.getElementById('vri-inspector') + expect(inspector.style.display).toBe('block') + }) + }) + describe('Inspector Panel Display', () => { it('should display all node metrics correctly', () => { createEnhancedVisualizerV2(mockProfiler)