/**
* ChromeDebugger Breakpoint Tests
*
* Integration tests for Chrome DevTools Protocol breakpoint functionality
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ChromeDebugger, BreakpointLocation, BreakpointOptions } from '../src/debuggers/ChromeDebugger.js';
import { ConfigManager } from '../src/config/ConfigManager.js';
// Mock dependencies
vi.mock('../src/config/ConfigManager.js');
describe('ChromeDebugger - Breakpoint Functionality', () => {
let chromeDebugger: ChromeDebugger;
let mockConfigManager: ConfigManager;
let mockCDPSession: any;
let mockPage: any;
let mockBrowser: any;
const mockConfig = {
browser: {
autoConnect: true,
port: 9222,
host: 'localhost',
timeout: 10000,
retryAttempts: 3,
retryDelay: 2000
}
};
beforeEach(() => {
// Create mock CDP session
mockCDPSession = {
send: vi.fn(),
on: vi.fn(),
detach: vi.fn()
};
// Create mock page
mockPage = {
target: vi.fn().mockReturnValue({
createCDPSession: vi.fn().mockResolvedValue(mockCDPSession)
}),
on: vi.fn()
};
// Create mock browser
mockBrowser = {
pages: vi.fn().mockResolvedValue([mockPage]),
newPage: vi.fn().mockResolvedValue(mockPage),
on: vi.fn(),
disconnect: vi.fn()
};
// Setup config manager mock
mockConfigManager = new ConfigManager();
vi.mocked(mockConfigManager.getConfig).mockReturnValue(mockConfig);
// Create ChromeDebugger instance
chromeDebugger = new ChromeDebugger(mockConfigManager);
// Mock the browser property (normally set during connect)
(chromeDebugger as any).browser = mockBrowser;
(chromeDebugger as any).page = mockPage;
(chromeDebugger as any).cdpSession = mockCDPSession;
(chromeDebugger as any).isConnected = true;
(chromeDebugger as any).debuggerEnabled = true;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Setting Breakpoints', () => {
it('should set a breakpoint by file path successfully', async () => {
const location: BreakpointLocation = {
filePath: 'src/test.js',
lineNumber: 10
};
const options: BreakpointOptions = {
condition: 'x > 5',
enabled: true
};
const mockCDPResponse = {
breakpointId: 'bp-123',
actualLocation: {
lineNumber: 9, // CDP uses 0-based line numbers
columnNumber: 0
}
};
mockCDPSession.send.mockResolvedValue(mockCDPResponse);
const result = await chromeDebugger.setBreakpoint(location, options);
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.setBreakpointByUrl', {
url: 'http://localhost:3000/src/test.js',
lineNumber: 9, // Converted to 0-based
condition: 'x > 5'
});
expect(result.id).toBeDefined();
expect(result.location.lineNumber).toBe(10); // Converted back to 1-based
expect(result.options.condition).toBe('x > 5');
expect(result.hitCount).toBe(0);
expect(result.enabled).toBe(true);
});
it('should set a breakpoint by URL successfully', async () => {
const location: BreakpointLocation = {
url: 'http://localhost:3000/app.js',
lineNumber: 15,
columnNumber: 5
};
const mockCDPResponse = {
breakpointId: 'bp-456',
actualLocation: {
lineNumber: 14,
columnNumber: 5
}
};
mockCDPSession.send.mockResolvedValue(mockCDPResponse);
const result = await chromeDebugger.setBreakpoint(location);
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.setBreakpointByUrl', {
url: 'http://localhost:3000/app.js',
lineNumber: 14,
columnNumber: 5
});
expect(result.location.url).toBe('http://localhost:3000/app.js');
expect(result.location.lineNumber).toBe(15);
expect(result.location.columnNumber).toBe(5);
});
it('should handle conditional breakpoints', async () => {
const location: BreakpointLocation = {
filePath: 'src/component.js',
lineNumber: 25
};
const options: BreakpointOptions = {
condition: 'props.isVisible === true'
};
const mockCDPResponse = {
breakpointId: 'bp-conditional',
actualLocation: { lineNumber: 24 }
};
mockCDPSession.send.mockResolvedValue(mockCDPResponse);
const result = await chromeDebugger.setBreakpoint(location, options);
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.setBreakpointByUrl', {
url: 'http://localhost:3000/src/component.js',
lineNumber: 24,
condition: 'props.isVisible === true'
});
expect(result.options.condition).toBe('props.isVisible === true');
});
it('should fail when debugger is not enabled', async () => {
(chromeDebugger as any).debuggerEnabled = false;
const location: BreakpointLocation = {
filePath: 'src/test.js',
lineNumber: 10
};
await expect(chromeDebugger.setBreakpoint(location)).rejects.toThrow(
'Debugger not enabled. Cannot set breakpoint.'
);
});
it('should fail when neither URL nor filePath is provided', async () => {
const location: BreakpointLocation = {
lineNumber: 10
};
await expect(chromeDebugger.setBreakpoint(location)).rejects.toThrow(
'Either URL or filePath must be provided'
);
});
it('should handle CDP errors gracefully', async () => {
const location: BreakpointLocation = {
filePath: 'src/test.js',
lineNumber: 10
};
mockCDPSession.send.mockRejectedValue(new Error('CDP Error: Invalid location'));
await expect(chromeDebugger.setBreakpoint(location)).rejects.toThrow('CDP Error: Invalid location');
});
});
describe('Removing Breakpoints', () => {
it('should remove a breakpoint successfully', async () => {
// First set a breakpoint
const location: BreakpointLocation = {
filePath: 'src/test.js',
lineNumber: 10
};
const mockSetResponse = {
breakpointId: 'bp-to-remove',
actualLocation: { lineNumber: 9 }
};
mockCDPSession.send.mockResolvedValueOnce(mockSetResponse);
await chromeDebugger.setBreakpoint(location);
// Now remove it
mockCDPSession.send.mockResolvedValueOnce(undefined);
const result = await chromeDebugger.removeBreakpoint('bp-to-remove');
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.removeBreakpoint', {
breakpointId: 'bp-to-remove'
});
expect(result).toBe(true);
});
it('should return false for non-existent breakpoint', async () => {
const result = await chromeDebugger.removeBreakpoint('non-existent');
expect(result).toBe(false);
expect(mockCDPSession.send).not.toHaveBeenCalled();
});
it('should fail when debugger is not enabled', async () => {
(chromeDebugger as any).debuggerEnabled = false;
await expect(chromeDebugger.removeBreakpoint('bp-123')).rejects.toThrow(
'Debugger not enabled. Cannot remove breakpoint.'
);
});
});
describe('Managing Breakpoints', () => {
it('should get all breakpoints', () => {
// Set up some breakpoints first
const breakpoints = chromeDebugger.getBreakpoints();
expect(Array.isArray(breakpoints)).toBe(true);
});
it('should get a specific breakpoint by ID', async () => {
// Set a breakpoint first
const location: BreakpointLocation = {
filePath: 'src/test.js',
lineNumber: 10
};
const mockCDPResponse = {
breakpointId: 'bp-specific',
actualLocation: { lineNumber: 9 }
};
mockCDPSession.send.mockResolvedValue(mockCDPResponse);
await chromeDebugger.setBreakpoint(location);
const breakpoint = chromeDebugger.getBreakpoint('bp-specific');
expect(breakpoint).toBeDefined();
expect(breakpoint?.location.filePath).toBe('src/test.js');
});
it('should return undefined for non-existent breakpoint', () => {
const breakpoint = chromeDebugger.getBreakpoint('non-existent');
expect(breakpoint).toBeUndefined();
});
it('should toggle breakpoints active/inactive', async () => {
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.setBreakpointsActive(false);
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.setBreakpointsActive', {
active: false
});
});
it('should clear all breakpoints', async () => {
// Set some breakpoints first
const location1: BreakpointLocation = { filePath: 'src/test1.js', lineNumber: 10 };
const location2: BreakpointLocation = { filePath: 'src/test2.js', lineNumber: 20 };
mockCDPSession.send
.mockResolvedValueOnce({ breakpointId: 'bp-1', actualLocation: { lineNumber: 9 } })
.mockResolvedValueOnce({ breakpointId: 'bp-2', actualLocation: { lineNumber: 19 } });
await chromeDebugger.setBreakpoint(location1);
await chromeDebugger.setBreakpoint(location2);
// Clear all breakpoints
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.clearAllBreakpoints();
expect(chromeDebugger.getBreakpoints()).toHaveLength(0);
});
});
describe('Debugger Control', () => {
it('should resume execution', async () => {
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.resume();
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.resume');
});
it('should step over', async () => {
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.stepOver();
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.stepOver');
});
it('should step into', async () => {
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.stepInto();
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.stepInto');
});
it('should step out', async () => {
mockCDPSession.send.mockResolvedValue(undefined);
await chromeDebugger.stepOut();
expect(mockCDPSession.send).toHaveBeenCalledWith('Debugger.stepOut');
});
});
describe('Utility Methods', () => {
it('should check if debugger is enabled', () => {
expect(chromeDebugger.isDebuggerEnabled()).toBe(true);
(chromeDebugger as any).debuggerEnabled = false;
expect(chromeDebugger.isDebuggerEnabled()).toBe(false);
});
it('should convert file path to URL correctly', () => {
const filePathToUrl = (chromeDebugger as any).filePathToUrl;
expect(filePathToUrl('src/test.js')).toBe('http://localhost:3000/src/test.js');
expect(filePathToUrl('/src/test.js')).toBe('http://localhost:3000/src/test.js');
expect(filePathToUrl('http://example.com/test.js')).toBe('http://example.com/test.js');
});
});
describe('Event Handling', () => {
it('should handle breakpoint hit events', () => {
const mockParams = {
callFrames: [{
callFrameId: 'frame-1',
functionName: 'testFunction',
location: { scriptId: 'script-1', lineNumber: 10 },
url: 'http://localhost:3000/test.js',
scopeChain: [
{
type: 'local',
name: 'Local',
startLocation: { scriptId: 'script-1', lineNumber: 10 },
endLocation: { scriptId: 'script-1', lineNumber: 20 },
object: { objectId: 'obj-1' }
}
]
}],
reason: 'breakpoint',
hitBreakpoints: ['bp-123']
};
// Set up a breakpoint first
const breakpoints = new Map();
breakpoints.set('bp-123', {
id: 'bp-123',
hitCount: 0,
options: {}
});
(chromeDebugger as any).breakpoints = breakpoints;
// Simulate the event handler
const handleBreakpointHit = (chromeDebugger as any).handleBreakpointHit;
handleBreakpointHit.call(chromeDebugger, mockParams);
// Verify hit count was incremented
const breakpoint = breakpoints.get('bp-123');
expect(breakpoint.hitCount).toBe(1);
});
it('should handle logpoint execution', async () => {
const mockBreakpoint = {
id: 'bp-logpoint',
options: {
logMessage: 'Debug: value is ${value}'
}
};
const mockHit = {
breakpointId: 'bp-logpoint',
callFrames: [{ callFrameId: 'frame-1' }]
};
mockCDPSession.send.mockResolvedValue({ result: { value: 'logged' } });
const handleLogpoint = (chromeDebugger as any).handleLogpoint;
await handleLogpoint.call(chromeDebugger, mockBreakpoint, mockHit);
expect(mockCDPSession.send).toHaveBeenCalledWith('Runtime.evaluate', {
expression: expect.stringContaining('console.log'),
contextId: 'frame-1'
});
});
});
});