项目文件夹

文件

299 行
9.6 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {
enableDebugger,
disableDebugger,
setBreakpoint,
removeBreakpoint,
setLogpoint,
removeLogpoint,
resume,
getPausedState,
evaluateOnCallFrame,
getScopeVariables,
// getCodeLines, // Not easy to test without a real script with multiple lines knowing external scriptId
} from '../../src/tools/debugger.js';
import {withMcpContext} from '../utils.js';
describe('debugger', () => {
it('enables and disables debugger', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await enableDebugger.handler({params: {}, page}, response, context);
assert.ok(response.responseLines[0].includes('Debugger enabled'));
response.resetResponseLineForTesting();
await disableDebugger.handler({params: {}, page}, response, context);
assert.ok(response.responseLines[0].includes('Debugger disabled'));
});
});
it('sets and removes breakpoint', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
// Navigate to a page to ensure we have a execution context
await page.pptrPage.setContent(
'<script>function test() { console.log("test"); }</script>',
);
await setBreakpoint.handler(
{params: {url: 'http://localhost/', lineNumber: 1}, page},
response,
context,
);
const setOutput = response.responseLines.join('\n');
const breakpointIdMatch = setOutput.match(/Breakpoint set with ID: (.*)/);
assert.ok(breakpointIdMatch, 'Should return breakpoint ID');
const breakpointId = breakpointIdMatch[1];
response.resetResponseLineForTesting();
await removeBreakpoint.handler(
{params: {breakpointId}, page},
response,
context,
);
assert.ok(
response.responseLines[0].includes(
`Breakpoint ${breakpointId} removed`,
),
);
});
});
it('sets and removes logpoint', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await page.pptrPage.setContent(
'<script>function test() { console.log("test"); }</script>',
);
await setLogpoint.handler(
{
params: {url: 'http://localhost/', lineNumber: 1, message: 'Log {x}'},
page,
},
response,
context,
);
const setOutput = response.responseLines.join('\n');
const breakpointIdMatch = setOutput.match(/Logpoint set with ID: (.*)/);
assert.ok(breakpointIdMatch, 'Should return logpoint ID');
const breakpointId = breakpointIdMatch[1];
// Verify condition includes logs
assert.ok(setOutput.includes('console.log(`Log ${x}`)'));
response.resetResponseLineForTesting();
await removeLogpoint.handler(
{params: {breakpointId}, page},
response,
context,
);
assert.ok(
response.responseLines[0].includes(`Logpoint ${breakpointId} removed`),
);
});
});
});
it('escapes backslashes in logpoint message', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await page.pptrPage.setContent(
'<script>function test() { console.log("test"); }</script>',
);
await setLogpoint.handler(
{
params: {
url: 'http://localhost/',
lineNumber: 1,
message: 'Path: C:\\Windows\\System32',
},
page,
},
response,
context,
);
const setOutput = response.responseLines.join('\n');
const breakpointIdMatch = setOutput.match(/Logpoint set with ID: (.*)/);
assert.ok(breakpointIdMatch, 'Should return logpoint ID');
// In generated code string: console.log(`Path: C:\\Windows\\System32`)
// But because it's inside a string in code, backslashes are escaped.
// Wait. The code string is `console.log("...")`.
// If we use backticks in code: `console.log(\`Path: C:\\\\Windows\\\\System32\`)`
// My fix: replace `\` with `\\`.
// Input: `C:\Windows`.
// Output string in `condition`: `console.log(`Path: C:\\Windows`)`
// Wait. `console.log` executes at runtime.
// `console.log(`C:\Windows`)` prints `C:Windows` (escapes W? no). `C:\Windows`.
// Check: `console.log(`C:\\Windows`)` prints `C:\Windows`.
// So we want the condition code to contain `console.log(`C:\\Windows`)`.
// So we need `runExpression` string to be `console.log(`C:\\Windows`)`.
// My code: `message.replace(/\\/g, '\\\\')`.
// Input `C:\Windows` -> `C:\\Windows`.
// Result `runExpression`: `console.log(`C:\\Windows`)`.
// This is what we want.
// So expected string in condition: `console.log(\`Path: C:\\\\Windows\\\\System32\`)`?
// No. `C:\\Windows`.
// In JS string literal for test assertion:
// We want to match `C:\\Windows`.
// Regex or string includes: `'C:\\\\Windows'`.
assert.ok(
setOutput.includes('Path: C:\\\\Windows\\\\System32'),
'Should double escape backslashes for template literal',
);
});
});
it('reports not paused when execution is running', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await enableDebugger.handler({params: {}, page}, response, context);
response.resetResponseLineForTesting();
await getPausedState.handler({params: {}, page}, response, context);
assert.ok(response.responseLines[0].includes('Debugger is not paused'));
});
});
it('pauses on breakpoint and resumes', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
const pptrPage = page.pptrPage;
// We need a script that runs somewhat later or triggered by us to hit the breakpoint reliably in test
// Or we can use `debugger;` statement.
await pptrPage.evaluate(() => {
// @ts-expect-error - Window property
window.debugMe = () => {
// eslint-disable-next-line no-debugger
debugger;
};
});
// Enable debugger
await enableDebugger.handler({params: {}, page}, response, context);
// Trigger debugger
// We trigger execution, but we usually need to do it without awaiting if it pauses.
await pptrPage.evaluate(() => {
setTimeout(() => {
// @ts-expect-error - Window property
window.debugMe();
}, 100);
});
// Wait a bit for pause
await new Promise(r => setTimeout(r, 500));
response.resetResponseLineForTesting();
await getPausedState.handler({params: {}, page}, response, context);
const output = response.responseLines.join('\n');
assert.ok(
output.includes('Paused state') ||
output.includes('Debugger is not paused'),
'Should report state (flake warning: might not be paused yet)',
);
if (output.includes('Paused state')) {
// Test resume
response.resetResponseLineForTesting();
await resume.handler({params: {}, page}, response, context);
assert.ok(response.responseLines[0].includes('Resumed execution'));
}
});
});
it('reports not enabled when calling getPausedState without enabling', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await getPausedState.handler({params: {}, page}, response, context);
assert.ok(response.responseLines[0].includes('Debugger is not enabled'));
});
});
it('throws when calling getScopeVariables without enabling', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
try {
await getScopeVariables.handler(
{params: {callFrameId: '1', scopeIndex: 0}, page},
response,
context,
);
assert.fail('Should have thrown');
} catch (e) {
const error = e as Error;
if (!error.message.includes('Debugger is not enabled')) {
console.error('Unexpected error:', e);
assert.fail(`Expected "Debugger is not enabled", got: ${e.message}`);
}
assert.ok(true);
}
});
});
it('evaluates on call frame (mock check)', async () => {
// This is hard to test e2e without actually being paused.
// We verified "not paused" error in getPausedState.
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
try {
await evaluateOnCallFrame.handler(
{params: {callFrameId: 'fake', expression: '1+1'}, page},
response,
context,
);
} catch (e) {
// It might fail because session throws "Invalid parameters" or similar from CDP
assert.ok(e);
}
});
});
it('removes logpoints via removeAllBreakpoints', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedMcpPage();
await page.pptrPage.setContent('<script>function test() {}</script>');
// Set a logpoint
await setLogpoint.handler(
{
params: {url: 'http://localhost/', lineNumber: 1, message: 'Log'},
page,
},
response,
context,
);
response.resetResponseLineForTesting();
// Remove all
// We need to import removeAllBreakpoints
const {removeAllBreakpoints} = await import('../../src/tools/debugger.js');
await removeAllBreakpoints.handler({params: {}, page}, response, context);
assert.ok(
response.responseLines[0].includes(
'All breakpoints and logpoints removed',
),
);
});
});