/**
* ChromeDebugger Variable Inspection Integration Tests
*
* Tests for Chrome DevTools Protocol integration for variable inspection,
* scope chain access, and expression evaluation.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ChromeDebugger, VariableDescriptor, ScopeDescriptor, EvaluationResult } from '../src/debuggers/ChromeDebugger.js';
import { ConfigManager } from '../src/config/ConfigManager.js';
// Mock dependencies
vi.mock('../src/config/ConfigManager.js');
describe('ChromeDebugger Variable Inspection', () => {
let chromeDebugger: ChromeDebugger;
let mockConfigManager: ConfigManager;
let mockCdpSession: any;
const mockConfig = {
browser: {
host: 'localhost',
port: 9222
}
};
beforeEach(() => {
vi.clearAllMocks();
mockConfigManager = new ConfigManager() as any;
mockConfigManager.getConfig = vi.fn().mockReturnValue(mockConfig);
// Mock CDP session
mockCdpSession = {
send: vi.fn(),
on: vi.fn(),
detach: vi.fn()
};
chromeDebugger = new ChromeDebugger(mockConfigManager);
// Inject mock CDP session
(chromeDebugger as any).cdpSession = mockCdpSession;
(chromeDebugger as any).debuggerEnabled = true;
(chromeDebugger as any).isPaused = true;
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Call Frame Management', () => {
it('should return empty call frames when not paused', async () => {
(chromeDebugger as any).isPaused = false;
const callFrames = await chromeDebugger.getCallFrames();
expect(callFrames).toEqual([]);
});
it('should return stored call frames when paused', async () => {
const mockCallFrames = [
{
callFrameId: 'frame_1',
functionName: 'testFunction',
location: { scriptId: 'script_1', lineNumber: 10 },
url: 'test.js',
scopeChain: [],
thisObject: {}
}
];
(chromeDebugger as any).currentCallFrames = mockCallFrames;
const callFrames = await chromeDebugger.getCallFrames();
expect(callFrames).toEqual(mockCallFrames);
});
it('should get top call frame', () => {
const mockCallFrames = [
{ callFrameId: 'frame_1', functionName: 'topFunction' },
{ callFrameId: 'frame_2', functionName: 'bottomFunction' }
];
(chromeDebugger as any).currentCallFrames = mockCallFrames;
const topFrame = chromeDebugger.getTopCallFrame();
expect(topFrame?.callFrameId).toBe('frame_1');
expect(topFrame?.functionName).toBe('topFunction');
});
it('should get call frame by ID', () => {
const mockCallFrames = [
{ callFrameId: 'frame_1', functionName: 'function1' },
{ callFrameId: 'frame_2', functionName: 'function2' }
];
(chromeDebugger as any).currentCallFrames = mockCallFrames;
const frame = chromeDebugger.getCallFrameById('frame_2');
expect(frame?.functionName).toBe('function2');
});
});
describe('Scope Chain Access', () => {
it('should get scope chain for call frame', async () => {
const mockScopeChain = [
{
type: 'local' as const,
objectId: 'scope_local',
variableCount: 3
},
{
type: 'closure' as const,
objectId: 'scope_closure',
variableCount: 1
}
];
const mockCallFrames = [
{
callFrameId: 'frame_1',
functionName: 'testFunction',
scopeChain: mockScopeChain
}
];
(chromeDebugger as any).currentCallFrames = mockCallFrames;
const scopeChain = await chromeDebugger.getScopeChain('frame_1');
expect(scopeChain).toEqual(mockScopeChain);
});
it('should throw error when call frame not found', async () => {
(chromeDebugger as any).currentCallFrames = [];
await expect(chromeDebugger.getScopeChain('nonexistent_frame'))
.rejects.toThrow('Call frame not found');
});
it('should throw error when debugger not paused', async () => {
(chromeDebugger as any).isPaused = false;
await expect(chromeDebugger.getScopeChain('frame_1'))
.rejects.toThrow('Debugger is not paused');
});
});
describe('Variable Inspection', () => {
it('should get variables in scope', async () => {
const mockCdpResponse = {
result: [
{
name: 'localVar',
value: { type: 'string', value: 'test', description: 'test' },
writable: true,
configurable: true,
enumerable: true,
isOwn: true
},
{
name: 'counter',
value: { type: 'number', value: 42, description: '42' },
writable: true,
configurable: true,
enumerable: true,
isOwn: true
}
]
};
mockCdpSession.send.mockResolvedValue(mockCdpResponse);
const variables = await chromeDebugger.getVariablesInScope('scope_123');
expect(mockCdpSession.send).toHaveBeenCalledWith('Runtime.getProperties', {
objectId: 'scope_123',
ownProperties: true,
accessorPropertiesOnly: false,
generatePreview: true,
nonIndexedPropertiesOnly: true
});
expect(variables).toHaveLength(2);
expect(variables[0].name).toBe('localVar');
expect(variables[0].value).toBe('test');
expect(variables[1].name).toBe('counter');
expect(variables[1].value).toBe(42);
});
it('should include non-enumerable properties when requested', async () => {
mockCdpSession.send.mockResolvedValue({ result: [] });
await chromeDebugger.getVariablesInScope('scope_123', true);
expect(mockCdpSession.send).toHaveBeenCalledWith('Runtime.getProperties', {
objectId: 'scope_123',
ownProperties: true,
accessorPropertiesOnly: false,
generatePreview: true,
nonIndexedPropertiesOnly: false
});
});
it('should handle CDP errors gracefully', async () => {
mockCdpSession.send.mockRejectedValue(new Error('CDP connection lost'));
await expect(chromeDebugger.getVariablesInScope('scope_123'))
.rejects.toThrow('CDP connection lost');
});
});
describe('Expression Evaluation', () => {
it('should evaluate expression on call frame', async () => {
const mockCdpResponse = {
result: {
type: 'string',
value: 'evaluated result',
description: 'evaluated result'
}
};
mockCdpSession.send.mockResolvedValue(mockCdpResponse);
const result = await chromeDebugger.evaluateOnCallFrame(
'frame_1',
'localVar + " test"'
);
expect(mockCdpSession.send).toHaveBeenCalledWith('Debugger.evaluateOnCallFrame', {
callFrameId: 'frame_1',
expression: 'localVar + " test"',
objectGroup: 'variable-inspection',
includeCommandLineAPI: false,
silent: false,
returnByValue: false,
generatePreview: true,
throwOnSideEffect: false,
timeout: undefined
});
expect(result.result).toBe('evaluated result');
expect(result.type).toBe('string');
expect(result.wasThrown).toBe(false);
});
it('should evaluate expression in global context', async () => {
const mockCdpResponse = {
result: {
type: 'string',
value: 'global result',
description: 'global result'
}
};
mockCdpSession.send.mockResolvedValue(mockCdpResponse);
const result = await chromeDebugger.evaluateExpression('window.location.href');
expect(mockCdpSession.send).toHaveBeenCalledWith('Runtime.evaluate', {
expression: 'window.location.href',
objectGroup: 'variable-inspection',
includeCommandLineAPI: false,
silent: false,
returnByValue: false,
generatePreview: true,
awaitPromise: false,
throwOnSideEffect: false,
timeout: undefined
});
expect(result.result).toBe('global result');
});
it('should handle evaluation exceptions', async () => {
const mockCdpResponse = {
result: { type: 'undefined' },
exceptionDetails: {
text: 'ReferenceError: undefined variable'
}
};
mockCdpSession.send.mockResolvedValue(mockCdpResponse);
const result = await chromeDebugger.evaluateOnCallFrame(
'frame_1',
'undefinedVariable'
);
expect(result.wasThrown).toBe(true);
expect(result.exceptionDetails?.text).toContain('ReferenceError');
});
it('should pass evaluation options correctly', async () => {
mockCdpSession.send.mockResolvedValue({ result: { type: 'undefined' } });
await chromeDebugger.evaluateOnCallFrame(
'frame_1',
'console.log("test")',
{
includeCommandLineAPI: true,
returnByValue: true,
throwOnSideEffect: true,
timeout: 1000
}
);
expect(mockCdpSession.send).toHaveBeenCalledWith('Debugger.evaluateOnCallFrame', {
callFrameId: 'frame_1',
expression: 'console.log("test")',
objectGroup: 'variable-inspection',
includeCommandLineAPI: true,
silent: false,
returnByValue: true,
generatePreview: true,
throwOnSideEffect: true,
timeout: 1000
});
});
});
describe('Variable Modification', () => {
it('should set variable value', async () => {
mockCdpSession.send.mockResolvedValue({});
await chromeDebugger.setVariableValue('frame_1', 0, 'counter', 100);
expect(mockCdpSession.send).toHaveBeenCalledWith('Debugger.setVariableValue', {
scopeNumber: 0,
variableName: 'counter',
newValue: { value: 100 },
callFrameId: 'frame_1'
});
});
it('should handle variable modification errors', async () => {
mockCdpSession.send.mockRejectedValue(new Error('Variable is read-only'));
await expect(chromeDebugger.setVariableValue('frame_1', 0, 'readOnlyVar', 100))
.rejects.toThrow('Variable is read-only');
});
});
describe('Paused State Management', () => {
it('should return correct paused state', () => {
expect(chromeDebugger.isPausedState()).toBe(true);
(chromeDebugger as any).isPaused = false;
expect(chromeDebugger.isPausedState()).toBe(false);
});
it('should update call frames on breakpoint hit', () => {
const mockCallFrames = [
{
callFrameId: 'frame_1',
functionName: 'testFunction',
location: { scriptId: 'script_1', lineNumber: 10 },
url: 'test.js',
scopeChain: [
{
type: 'local',
object: { objectId: 'scope_local' }
}
],
this: {}
}
];
// Simulate breakpoint hit
(chromeDebugger as any).handleBreakpointHit({
callFrames: mockCallFrames,
reason: 'breakpoint',
hitBreakpoints: ['bp_1']
});
const storedFrames = (chromeDebugger as any).currentCallFrames;
expect(storedFrames).toHaveLength(1);
expect(storedFrames[0].callFrameId).toBe('frame_1');
expect(storedFrames[0].scopeChain[0].objectId).toBe('scope_local');
});
});
});