/**
* VariableManager Unit Tests
*
* Tests for variable inspection state management, watch expressions,
* and scope chain caching functionality.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
// Mock Logger first, before any other imports
vi.mock('../src/utils/Logger.js', () => ({
Logger: vi.fn().mockImplementation(() => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
setLevel: vi.fn()
}))
}));
// Mock other dependencies
vi.mock('../src/config/ConfigManager.js');
vi.mock('../src/debuggers/ChromeDebugger.js');
import { EventEmitter } from 'events';
import { VariableManager, VariableCommand, WatchExpression } from '../src/managers/VariableManager.js';
import { ConfigManager } from '../src/config/ConfigManager.js';
import { ChromeDebugger, VariableDescriptor, ScopeDescriptor, EvaluationResult } from '../src/debuggers/ChromeDebugger.js';
describe('VariableManager', () => {
let variableManager: VariableManager;
let mockConfigManager: ConfigManager;
let mockChromeDebugger: ChromeDebugger;
const mockConfig = {
variables: {
maxWatchExpressions: 10,
autoEvaluateWatches: true,
cacheVariables: true,
cacheDurationMs: 5000,
enableRealTimeUpdates: true
}
};
const mockCallFrameId = 'frame_123';
const mockScopeChain: ScopeDescriptor[] = [
{
type: 'local',
name: 'Local',
objectId: 'scope_local_123',
variableCount: 3
},
{
type: 'closure',
name: 'Closure',
objectId: 'scope_closure_123',
variableCount: 2
}
];
const mockVariables: VariableDescriptor[] = [
{
name: 'localVar',
value: 'test value',
type: 'string',
description: 'test value',
writable: true,
configurable: true,
enumerable: true,
isOwn: true
},
{
name: 'counter',
value: 42,
type: 'number',
description: '42',
writable: true,
configurable: true,
enumerable: true,
isOwn: true
}
];
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Create mock instances
mockConfigManager = new ConfigManager() as any;
mockChromeDebugger = new ChromeDebugger(mockConfigManager) as any;
// Setup mock implementations
mockConfigManager.getConfig = vi.fn().mockReturnValue(mockConfig);
mockConfigManager.on = vi.fn();
mockChromeDebugger.isPausedState = vi.fn().mockReturnValue(true);
mockChromeDebugger.getTopCallFrame = vi.fn().mockReturnValue({ callFrameId: mockCallFrameId });
mockChromeDebugger.getScopeChain = vi.fn().mockResolvedValue(mockScopeChain);
mockChromeDebugger.getVariablesInScope = vi.fn().mockResolvedValue(mockVariables);
mockChromeDebugger.evaluateOnCallFrame = vi.fn();
mockChromeDebugger.evaluateExpression = vi.fn();
mockChromeDebugger.setVariableValue = vi.fn();
mockChromeDebugger.on = vi.fn();
// Create VariableManager instance
variableManager = new VariableManager(mockConfigManager, mockChromeDebugger);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Configuration', () => {
it('should load default configuration', () => {
const config = variableManager.getConfig();
expect(config.maxWatchExpressions).toBe(10);
expect(config.autoEvaluateWatches).toBe(true);
expect(config.cacheVariables).toBe(true);
});
it('should handle missing configuration gracefully', () => {
mockConfigManager.getConfig = vi.fn().mockReturnValue({});
const newManager = new VariableManager(mockConfigManager, mockChromeDebugger);
const config = newManager.getConfig();
expect(config.maxWatchExpressions).toBe(50); // default value
expect(config.autoEvaluateWatches).toBe(true); // default value
});
});
describe('Variable Inspection', () => {
it('should inspect variables in local scope', async () => {
const command: VariableCommand = {
action: 'inspect',
callFrameId: mockCallFrameId,
scopeNumber: 0
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.variables).toEqual(mockVariables);
expect(result.scopeType).toBe('local');
expect(mockChromeDebugger.getScopeChain).toHaveBeenCalledWith(mockCallFrameId);
expect(mockChromeDebugger.getVariablesInScope).toHaveBeenCalledWith('scope_local_123', false);
});
it('should use top call frame when none specified', async () => {
const command: VariableCommand = {
action: 'inspect'
};
await variableManager.executeCommand(command);
expect(mockChromeDebugger.getTopCallFrame).toHaveBeenCalled();
expect(mockChromeDebugger.getScopeChain).toHaveBeenCalledWith(mockCallFrameId);
});
it('should handle scope index out of bounds', async () => {
const command: VariableCommand = {
action: 'inspect',
callFrameId: mockCallFrameId,
scopeNumber: 5 // Out of bounds
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('Scope index 5 out of bounds');
});
it('should fail when debugger is not paused', async () => {
mockChromeDebugger.isPausedState = vi.fn().mockReturnValue(false);
const command: VariableCommand = {
action: 'inspect'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('Debugger is not paused');
});
it('should include non-enumerable properties when requested', async () => {
const command: VariableCommand = {
action: 'inspect',
callFrameId: mockCallFrameId,
options: {
includeNonEnumerable: true
}
};
await variableManager.executeCommand(command);
expect(mockChromeDebugger.getVariablesInScope).toHaveBeenCalledWith('scope_local_123', true);
});
});
describe('Variable Caching', () => {
it('should cache variables when enabled', async () => {
const command: VariableCommand = {
action: 'inspect',
callFrameId: mockCallFrameId,
scopeNumber: 0
};
// First call
await variableManager.executeCommand(command);
expect(mockChromeDebugger.getVariablesInScope).toHaveBeenCalledTimes(1);
// Second call should use cache
await variableManager.executeCommand(command);
expect(mockChromeDebugger.getVariablesInScope).toHaveBeenCalledTimes(1); // Still 1, not called again
});
it('should provide cache statistics', () => {
const stats = variableManager.getCacheStats();
expect(stats).toHaveProperty('size');
expect(stats).toHaveProperty('keys');
expect(Array.isArray(stats.keys)).toBe(true);
});
});
describe('Expression Evaluation', () => {
it('should evaluate expression in call frame context', async () => {
const mockEvalResult: EvaluationResult = {
result: 'evaluated result',
type: 'string',
description: 'evaluated result',
wasThrown: false
};
mockChromeDebugger.evaluateOnCallFrame = vi.fn().mockResolvedValue(mockEvalResult);
const command: VariableCommand = {
action: 'evaluate',
expression: 'localVar + " test"',
callFrameId: mockCallFrameId
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.evaluationResult).toEqual(mockEvalResult);
expect(mockChromeDebugger.evaluateOnCallFrame).toHaveBeenCalledWith(
mockCallFrameId,
'localVar + " test"',
{}
);
});
it('should evaluate expression in global context when no call frame', async () => {
const mockEvalResult: EvaluationResult = {
result: 'global result',
type: 'string',
description: 'global result',
wasThrown: false
};
mockChromeDebugger.evaluateExpression = vi.fn().mockResolvedValue(mockEvalResult);
mockChromeDebugger.isPausedState = vi.fn().mockReturnValue(false);
const command: VariableCommand = {
action: 'evaluate',
expression: 'window.location.href'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(mockChromeDebugger.evaluateExpression).toHaveBeenCalledWith(
'window.location.href',
{}
);
});
it('should handle evaluation exceptions', async () => {
const mockEvalResult: EvaluationResult = {
result: undefined,
type: 'undefined',
description: '',
wasThrown: true,
exceptionDetails: { text: 'ReferenceError: undefined variable' }
};
mockChromeDebugger.evaluateOnCallFrame = vi.fn().mockResolvedValue(mockEvalResult);
const command: VariableCommand = {
action: 'evaluate',
expression: 'undefinedVariable',
callFrameId: mockCallFrameId
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('threw an exception');
});
it('should pass evaluation options correctly', async () => {
const command: VariableCommand = {
action: 'evaluate',
expression: 'console.log("test")',
callFrameId: mockCallFrameId,
options: {
includeCommandLineAPI: true,
returnByValue: true,
throwOnSideEffect: true,
timeout: 1000
}
};
await variableManager.executeCommand(command);
expect(mockChromeDebugger.evaluateOnCallFrame).toHaveBeenCalledWith(
mockCallFrameId,
'console.log("test")',
{
includeCommandLineAPI: true,
returnByValue: true,
throwOnSideEffect: true,
timeout: 1000
}
);
});
});
describe('Variable Modification', () => {
it('should modify variable value', async () => {
const command: VariableCommand = {
action: 'modify',
callFrameId: mockCallFrameId,
scopeNumber: 0,
variableName: 'counter',
newValue: 100
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.message).toContain('modified successfully');
expect(mockChromeDebugger.setVariableValue).toHaveBeenCalledWith(
mockCallFrameId,
0,
'counter',
100
);
});
it('should require all parameters for modification', async () => {
const command: VariableCommand = {
action: 'modify',
callFrameId: mockCallFrameId
// Missing required parameters
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('required for variable modification');
});
it('should fail when debugger is not paused', async () => {
mockChromeDebugger.isPausedState = vi.fn().mockReturnValue(false);
const command: VariableCommand = {
action: 'modify',
callFrameId: mockCallFrameId,
scopeNumber: 0,
variableName: 'counter',
newValue: 100
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('Debugger is not paused');
});
});
describe('Watch Expressions', () => {
it('should add a new watch expression', async () => {
const command: VariableCommand = {
action: 'watch',
expression: 'counter * 2',
watchAction: 'add'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.message).toContain('Watch expression added');
expect(result.watchExpressions).toHaveLength(1);
expect(result.watchExpressions![0].expression).toBe('counter * 2');
});
it('should list all watch expressions', async () => {
// Add a watch expression first
await variableManager.executeCommand({
action: 'watch',
expression: 'localVar',
watchAction: 'add'
});
const command: VariableCommand = {
action: 'watch',
watchAction: 'list'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.watchExpressions).toHaveLength(1);
expect(result.watchExpressions![0].expression).toBe('localVar');
});
it('should remove a watch expression', async () => {
// Add a watch expression first
const addResult = await variableManager.executeCommand({
action: 'watch',
expression: 'counter',
watchAction: 'add'
});
const watchId = addResult.watchExpressions![0].id;
const command: VariableCommand = {
action: 'watch',
watchId,
watchAction: 'remove'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.message).toContain('Watch expression removed');
});
it('should clear all watch expressions', async () => {
// Add multiple watch expressions
await variableManager.executeCommand({
action: 'watch',
expression: 'counter',
watchAction: 'add'
});
await variableManager.executeCommand({
action: 'watch',
expression: 'localVar',
watchAction: 'add'
});
const command: VariableCommand = {
action: 'watch',
watchAction: 'clear'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.message).toContain('Cleared 2 watch expressions');
});
it('should evaluate watch expressions', async () => {
const mockEvalResult: EvaluationResult = {
result: 84,
type: 'number',
description: '84',
wasThrown: false
};
mockChromeDebugger.evaluateOnCallFrame = vi.fn().mockResolvedValue(mockEvalResult);
// Add a watch expression
await variableManager.executeCommand({
action: 'watch',
expression: 'counter * 2',
watchAction: 'add'
});
const command: VariableCommand = {
action: 'watch',
callFrameId: mockCallFrameId,
watchAction: 'evaluate'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.watchExpressions).toHaveLength(1);
expect(result.watchExpressions![0].result).toBe(84);
});
it('should enforce maximum watch expression limit', async () => {
// Set a low limit for testing
mockConfigManager.getConfig = vi.fn().mockReturnValue({
variables: { ...mockConfig.variables, maxWatchExpressions: 2 }
});
const newManager = new VariableManager(mockConfigManager, mockChromeDebugger);
// Add maximum number of watch expressions
await newManager.executeCommand({
action: 'watch',
expression: 'expr1',
watchAction: 'add'
});
await newManager.executeCommand({
action: 'watch',
expression: 'expr2',
watchAction: 'add'
});
// Try to add one more
const result = await newManager.executeCommand({
action: 'watch',
expression: 'expr3',
watchAction: 'add'
});
expect(result.success).toBe(false);
expect(result.message).toContain('Maximum watch expressions limit reached');
});
it('should handle watch expression evaluation errors', async () => {
const mockEvalResult: EvaluationResult = {
result: undefined,
type: 'undefined',
description: '',
wasThrown: true,
exceptionDetails: { text: 'ReferenceError: undefined variable' }
};
mockChromeDebugger.evaluateOnCallFrame = vi.fn().mockResolvedValue(mockEvalResult);
// Add a watch expression
await variableManager.executeCommand({
action: 'watch',
expression: 'undefinedVar',
watchAction: 'add'
});
const command: VariableCommand = {
action: 'watch',
callFrameId: mockCallFrameId,
watchAction: 'evaluate'
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(true);
expect(result.watchExpressions![0].error).toContain('ReferenceError');
});
});
describe('Event Handling', () => {
it('should emit events for watch expression changes', async () => {
const watchAddedSpy = vi.fn();
const watchRemovedSpy = vi.fn();
variableManager.on('watchAdded', watchAddedSpy);
variableManager.on('watchRemoved', watchRemovedSpy);
// Add a watch expression
const addResult = await variableManager.executeCommand({
action: 'watch',
expression: 'counter',
watchAction: 'add'
});
expect(watchAddedSpy).toHaveBeenCalledWith(addResult.watchExpressions![0]);
// Remove the watch expression
const watchId = addResult.watchExpressions![0].id;
await variableManager.executeCommand({
action: 'watch',
watchId,
watchAction: 'remove'
});
expect(watchRemovedSpy).toHaveBeenCalled();
});
it('should handle debugger paused events', () => {
const pausedSpy = vi.fn();
variableManager.on('debuggerPaused', pausedSpy);
// Simulate debugger paused event
const pausedData = { callFrames: [{ callFrameId: mockCallFrameId }] };
mockChromeDebugger.emit('debuggerPaused', pausedData);
expect(pausedSpy).toHaveBeenCalledWith(pausedData);
});
it('should handle debugger resumed events', () => {
const resumedSpy = vi.fn();
variableManager.on('debuggerResumed', resumedSpy);
// Simulate debugger resumed event
mockChromeDebugger.emit('debuggerResumed');
expect(resumedSpy).toHaveBeenCalled();
});
});
describe('Error Handling', () => {
it('should handle unknown actions gracefully', async () => {
const command: VariableCommand = {
action: 'unknown' as any
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('Unknown variable action');
});
it('should handle Chrome debugger errors', async () => {
mockChromeDebugger.getScopeChain = vi.fn().mockRejectedValue(new Error('CDP connection lost'));
const command: VariableCommand = {
action: 'inspect',
callFrameId: mockCallFrameId
};
const result = await variableManager.executeCommand(command);
expect(result.success).toBe(false);
expect(result.message).toContain('CDP connection lost');
});
});
});